1 /**
2  * Encrypted RPC using vibe-noisestream.
3  *
4  * This module provides RPC client and server using encrypted noise streams.
5  * This requires the vibe-noisestream library and this module is therefore
6  * in an extra dub subpackage: vibe-rpcchannel:noise.
7  */
8 module vibe.rpcchannel.noise;
9 
10 import vibe.core.net;
11 import vibe.noise;
12 import vibe.rpcchannel.server;
13 import vibe.rpcchannel.client;
14 
15 /**
16  * Information about a noise connection.
17  * 
18  * Contains the TCPConnection as well as the NoiseStream. The NoiseStream can
19  * be queried for the public key of the remote end using the remoteKey property.
20  * This can be used to implement public key based authentication.
21  */
22 struct NoiseInfo
23 {
24     NoiseStream stream;
25     TCPConnection conn;
26 }
27 
28 /**
29  * The RPC server implementation using an encrypted vibe-noisestream transport stream.
30  */
31 class NoiseServer(Implementation, API)
32 {
33 private:
34     static struct NoiseServerSession
35     {
36         ServerSession!API session;
37         TCPConnection conn;
38     }
39 
40     TCPListener[] _listener;
41     NoiseServerSession[] _sessions;
42     NoiseSettings _noiseSettings;
43 
44     /*
45      * Called if other side disconnected first.
46      */
47     void onDisconnect(ServerSession!API session)
48     {
49         size_t j = 0;
50         for (size_t i = 0; i < _sessions.length; i++)
51         {
52             if (_sessions[i].session != session)
53             {
54                 _sessions[j] = _sessions[i];
55                 j++;
56             }
57         }
58         _sessions.length = j;
59     }
60 
61     /*
62      * Called when TCP listener receives a new connection.
63      */
64     void onTCPConnect(TCPConnection conn)
65     {
66         NoiseServerSession server;
67 
68         auto stream = conn.createNoiseStream(_noiseSettings);
69 
70         server.session = createServerSession!(Implementation, API)(stream, NoiseInfo(stream,
71             conn));
72         server.conn = conn;
73         _sessions ~= server;
74         scope (exit)
75         {
76             stream.finalize();
77             onDisconnect(server.session);
78         }
79         server.session.run();
80     }
81 
82 public:
83     /**
84      * Stop listening for new connections and cleanly disconnect all
85      * existing connections.
86      */
87     void stop()
88     {
89         foreach (listener; _listener)
90             listener.stopListening();
91 
92         foreach (session; _sessions)
93         {
94             session.session.disconnect();
95         }
96     }
97 }
98 
99 /**
100  * Create a new vibe-noisestream based API server.
101  */
102 NoiseServer!(Implementation, API) serveNoise(Implementation, API)(NoiseSettings settings,
103     ushort port) if (isServerAPI!(Implementation, NoiseInfo))
104 {
105     auto server = new NoiseServer!(Implementation, API)();
106     server._noiseSettings = settings;
107     server._listener = listenTCP(port, &server.onTCPConnect);
108     return server;
109 }
110 
111 ///
112 unittest
113 {
114     abstract static class API
115     {
116         string someMethod(string name);
117     }
118 
119     static class Implementation : API
120     {
121         static Implementation startSession(NoiseInfo info)
122         {
123             return new Implementation();
124         }
125 
126         override string someMethod(string name)
127         {
128             return "Hello " ~ name ~ "!";
129         }
130     }
131 
132     // See vibe-noisestream for how to generate keys
133     createKeys("server.key", "server.pub");
134     createKeys("client.key", "client.pub");
135 
136     // Need to set settings correctly. See vibe-noisestream documentation
137     // for more information. It's also possible to use a callback for remote
138     // key verification.
139     auto settings = NoiseSettings(NoiseKind.server);
140     settings.privateKeyPath = Path("server.key");
141     settings.remoteKeyPath = Path("client.pub");
142 
143     auto server = serveNoise!(Implementation, API)(settings, 8030);
144     server.stop();
145 }
146 
147 ///ditto
148 NoiseServer!(Implementation, API) serveNoise(Implementation, API)(
149     NoiseSettings settings, ushort port, string address) if (
150         isServerAPI!(Implementation, NoiseInfo))
151 {
152     auto server = new NoiseServer!(Implementation, API)();
153     server._noiseSettings = settings;
154     server._listener = [listenTCP(port, &server.onTCPConnect, address)];
155     return server;
156 }
157 
158 /**
159  * An alias for a vibe-noisestream based RPCClient.
160  */
161 template NoiseClient(API)
162 {
163     alias NoiseClient = RPCClient!(API, NoiseInfo);
164 }
165 
166 /**
167  * Connect to a remote, vibe-noisestream based API server.
168  *
169  * Examples:
170  * -------
171  * abstract static class API
172  * {
173  *     string someMethod(string name);
174  * }
175  *
176  * auto settings = NoiseSettings(NoiseKind.client);
177  * settings.privateKeyPath = Path("client.key");
178  * settings.remoteKeyPath = Path("server.pub");
179  *
180  * auto client = clientNoise!API(settings, "127.0.0.1", 8030);
181  * client.someMethod("john");
182  * client.closeTCP();
183  * ------
184  */
185 auto clientNoise(API)(NoiseSettings settings, string host, ushort port,
186     string bind_interface = null, ushort bind_port = cast(ushort) 0u)
187 {
188     auto conn = connectTCP(host, port, bind_interface, bind_port);
189     auto stream = conn.createNoiseStream(settings);
190     return createClientSession!(API, NoiseInfo)(stream, NoiseInfo(stream, conn));
191 }
192 
193 ///ditto
194 auto clientNoise(API)(NoiseSettings settings, NetworkAddress addr,
195     NetworkAddress bind_address = anyAddress())
196 {
197     auto conn = connectTCP(addr, bind_address);
198     auto stream = conn.createNoiseStream(settings);
199     return createClientSession!(API, NoiseInfo)(stream, NoiseInfo(stream, conn));
200 }
201 
202 void closeNoise(Client)(Client client)
203 {
204     client.disconnect();
205     client.connectionInfo.stream.finalize();
206     if (client.connectionInfo.conn.connected)
207     {
208         client.connectionInfo.conn.close();
209     }
210 }
211 
212 version (unittest)
213 {
214     import vibe.d, tinyevent, std.stdio;
215 
216     abstract class API
217     {
218         void request1();
219         void startEvents();
220         void stopEvents();
221         Event!() counterEvent;
222     }
223 
224     class APIServer : API
225     {
226         NoiseInfo _info;
227         bool _eventStop = false;
228 
229         this(NoiseInfo info)
230         {
231             _info = info;
232         }
233 
234         static APIServer startSession(NoiseInfo info)
235         {
236             return new APIServer(info);
237         }
238 
239         override void request1()
240         {
241         }
242 
243         override void startEvents()
244         {
245             runTask(() {
246                 while (!_eventStop)
247                 {
248                     counterEvent.emit();
249                     sleep(1000.usecs);
250                 }
251             });
252         }
253 
254         override void stopEvents()
255         {
256             _eventStop = true;
257         }
258     }
259 }
260 
261 // Test heavy event / call interleaving to make sure data does not get
262 // interleaved on the connection
263 // Test basic functions
264 unittest
265 {
266     uint done = 0;
267     NoiseServer!(APIServer, API) server;
268 
269     void clientMain()
270     {
271         try
272         {
273             scope (exit)
274             {
275                 done++;
276                 if (done == 100)
277                 {
278                     // We eant to test that the client disconnects first!
279                     sleep(10.msecs);
280                     server.stop();
281                     exitEventLoop();
282                 }
283             }
284 
285             auto settings = NoiseSettings(NoiseKind.client);
286             settings.privateKeyPath = Path("client.key");
287             settings.remoteKeyPath = Path("server.pub");
288             auto client = clientNoise!(API)(settings, "localhost", 8020);
289             client.startEvents();
290             for (size_t i = 0; i < 100; i++)
291             {
292                 client.request1();
293             }
294             client.stopEvents();
295             client.closeNoise();
296         }
297         catch (Exception e)
298             assert(false, "Should not throw: " ~ e.toString());
299     }
300 
301     createKeys("server.key", "server.pub");
302     createKeys("client.key", "client.pub");
303 
304     auto settings = NoiseSettings(NoiseKind.server);
305     settings.privateKeyPath = Path("server.key");
306     settings.remoteKeyPath = Path("client.pub");
307 
308     server = serveNoise!(APIServer, API)(settings, 8020);
309     for (size_t i = 0; i < 100; i++)
310         runTask(&clientMain);
311     runEventLoop();
312 }