1 /** 2 * Copyright © DiamondMVC 2019 3 * License: MIT (https://github.com/DiamondMVC/Diamond/blob/master/LICENSE) 4 * Author: Jacob Jensen (bausshf) 5 */ 6 module diamond.http.websockets; 7 8 import std.variant : Variant; 9 import std.conv : to; 10 11 import vibewebsockets = vibe.http.websockets; 12 13 import diamond.errors; 14 15 /// Collection of web socket services; 16 private __gshared WebSocketService[string] _webSocketServices; 17 18 /// Wrapper around a web socket. 19 final class WebSocket 20 { 21 private: 22 /// Boolean determining whether the websocket is in strict mode or not. 23 bool _strict; 24 /// The raw web socket. 25 vibewebsockets.WebSocket _socket; 26 /// The websocket context. 27 Variant[string] _context; 28 29 /** 30 * Creates a new websocket. 31 * Params: 32 * socket = The raw socket. 33 * strict = Boolean determining whether the websocket is in strict mode or not. 34 */ 35 this(vibewebsockets.WebSocket socket, bool strict) 36 { 37 _socket = socket; 38 _strict = strict; 39 } 40 41 public: 42 final: 43 package(diamond) 44 { 45 /// Waits for data to be received. 46 bool waitForData() 47 { 48 return _socket.waitForData(); 49 } 50 } 51 52 /// Reads the current message as a buffer. 53 ubyte[] readBuffer() 54 { 55 return _socket.receiveBinary(); 56 } 57 58 /// Reads the current message as a text string. 59 string readText() 60 { 61 return _socket.receiveText(); 62 } 63 64 /// Reads the current message as a generic data-type. 65 T read(T)() 66 { 67 return to!T(readText()); 68 } 69 70 /** 71 * Waits for the next message and reads it as a buffer. 72 * Params: 73 * (out) buffer = The buffer. 74 * Returns: 75 * True if the message was received, false otherwise (Ex. connection closed.) 76 */ 77 bool readBufferNext(out ubyte[] buffer) 78 { 79 buffer = null; 80 waitForData(); 81 82 if (!_socket.connected) 83 { 84 return false; 85 } 86 87 buffer = _socket.receiveBinary(); 88 return true; 89 } 90 91 /** 92 * Waits for the next message and reads it as a text string. 93 * Params: 94 * (out) text = The text. 95 * Returns: 96 * True if the message was received, false otherwise (Ex. connection closed.) 97 */ 98 bool readTextNext(out string text) 99 { 100 text = null; 101 waitForData(); 102 103 if (!_socket.connected) 104 { 105 return false; 106 } 107 108 text = _socket.receiveText(); 109 return true; 110 } 111 112 /** 113 * Waits for the next message and reads it as a generic data-type. 114 * Params: 115 * (out) value = The value. 116 * Returns: 117 * True if the message was received, false otherwise (Ex. connection closed.) 118 */ 119 bool readNext(T)(out T value) 120 { 121 value = T.init; 122 waitForData(); 123 124 if (!_socket.connected) 125 { 126 return false; 127 } 128 129 value = to!T(readText()); 130 131 return true; 132 } 133 134 /** 135 * Sends a buffer to the web socket. 136 * Params: 137 * buffer = The buffer to send. 138 */ 139 void sendBuffer(ubyte[] buffer) 140 { 141 _socket.send(buffer); 142 } 143 144 /** 145 * Sends a text string to the web socket. 146 * Params: 147 * text = The text to send. 148 */ 149 void sendText(string text) 150 { 151 _socket.send(text); 152 } 153 154 /** 155 * Sends a generic data value to the web socket. 156 * Params: 157 * value = The value to send. 158 */ 159 void send(T)(T value) 160 { 161 _socket.send(to!string(value)); 162 } 163 164 /** 165 * Closes the web socket. 166 * Params: 167 * code = The termination code. 168 * reason = A reason given, why the websocket has been closed. 169 */ 170 void close(short code = 0, string reason = "") 171 { 172 _socket.close(code, reason); 173 } 174 175 /** 176 * Adds context data to the web socket. 177 * Params: 178 * name = The name of the context data. 179 * data = The data to add. 180 */ 181 void add(T)(string name, T data) 182 { 183 _context[name] = data; 184 } 185 186 /** 187 * Gets the context data of the web socket. 188 * Params: 189 * name = The name of the context data. 190 * Returns: 191 * The context data if found, defaultValue otherwise. 192 */ 193 T get(T)(string name, lazy T defaultValue = T.init) 194 { 195 auto data = _context.get(name, Variant.init); 196 197 if (!data.hasValue) 198 { 199 return defaultValue; 200 } 201 202 return data.get!T; 203 } 204 } 205 206 /// Wrapper around a websocket service. 207 abstract class WebSocketService 208 { 209 private: 210 /// The route of the service. 211 string _route; 212 /// Boolean determining whether web socket service is in strict mode or not. 213 bool _strict; 214 215 public: 216 /** 217 * Creates a new web socket service. 218 * Params: 219 * route = The route of the web socket. 220 * strict = Boolean determ 221 */ 222 this(string route, bool strict = true) 223 { 224 _route = route; 225 _strict = strict; 226 } 227 228 package(diamond) 229 { 230 /** 231 * Handling the raw web socket. 232 * Params: 233 * rawSocket = The raw socket. 234 */ 235 final void handleWebSocket(scope vibewebsockets.WebSocket rawSocket) 236 { 237 auto socket = new WebSocket(rawSocket, _strict); 238 239 onConnect(socket); 240 241 while (socket.waitForData()) 242 { 243 onMessage(socket); 244 } 245 246 onClose(socket); 247 } 248 } 249 250 @property 251 { 252 /// Gets the route of the service. 253 final string route() { return _route; } 254 } 255 256 /// Function called when a web socket connects. 257 abstract void onConnect(WebSocket socket); 258 259 /// Function called when a web socket has received a message. 260 abstract void onMessage(WebSocket socket); 261 262 /// Function called when a web socket is closed. 263 abstract void onClose(WebSocket socket); 264 } 265 266 /** 267 * Adds a web socket service. 268 * Params: 269 * service = The web socket service to add. 270 */ 271 void addWebSocketService(WebSocketService service) 272 { 273 enforce(service, "No web socket service specified."); 274 275 _webSocketServices[service.route] = service; 276 } 277 278 package(diamond) 279 { 280 import vibe.d : URLRouter; 281 282 /** 283 * Handles web sockets. 284 * Params: 285 * router = The router. 286 */ 287 void handleWebSockets(URLRouter router) 288 { 289 enforce(router, "Found no router"); 290 291 if (!_webSocketServices) 292 { 293 return; 294 } 295 296 foreach (service; _webSocketServices) 297 { 298 router.get(service.route, vibewebsockets.handleWebSockets((scope socket) 299 { 300 auto service = _webSocketServices.get(socket.request.requestPath.toString(), null); 301 302 if (!service) 303 { 304 socket.close(); 305 return; 306 } 307 308 service.handleWebSocket(socket); 309 })); 310 } 311 } 312 }