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 }