1 /++
2     socketplate – “app entry point” helper module
3 
4     Also provides all necessary $(I public imports) to quickstart coding.
5 
6     ## Developer manual
7 
8     Hello and welcome to socketplate.
9 
10     ### How to get started
11 
12     socketplate.app provides to entry points:
13 
14     $(LIST
15         * Single connection handler
16         * Manual handler setup
17     )
18 
19     Both come with batteries included:
20 
21     $(LIST
22         * Several command-line configuration options (worker count etc.)
23         * Built-in `--help` parameter
24         * Privilege dropping (on POSIX)
25         * Specify a custom app name
26         * Custom tuning of the socket server (see [socketplate.server.server.SocketServerTunables])
27     )
28 
29     All that’s left to do is to pick either of the entry points
30     and call the corresponding `runSocketplateApp` overload from your `main()` function.
31 
32     #### Single connection handler
33 
34     In this mode listening addresses are read from `args`
35     that will usually point to the program’s command line args.
36 
37     This way, an end-user can specify listening addresses by passing `-S <socket>` parameters.
38 
39     ```
40     $ ./your-app --S 127.0.0.1:8080
41     $ ./your-app --S [::1]:8080
42     $ ./your-app
43     ```
44 
45     Sample code:
46 
47     ---
48     import socketplate.app;
49 
50     int main(string[] args)
51     {
52         return runSocketplateApp("my app", args, (SocketConnection connection) {
53             // your code here…
54         });
55     }
56     ---
57 
58     There’s no need to give up $(B default listening addresses) either.
59     Provide an array of them as the 4th parameter of the `runSocketplateApp` function.
60     If default listening addresses are provided, the server will use them when non are specified through `args`.
61 
62     ---
63     string[] defaultListeningAddresses = [
64         "127.0.0.1:8080",
65         "[::1]:8080",
66     ];
67 
68     return runSocketplateApp("my app", args, (SocketConnection connection) {
69         // your code here…
70     }, defaultListeningAddresses);
71     ---
72 
73     #### Manual handler setup
74 
75     This variation allows you to setup listeners through code instead.
76 
77     Code sample:
78 
79     ---
80     import socketplate.app;
81 
82     int main(string[] args)
83     {
84         return runSocketplateApp("my app", args, (SocketServer server)
85         {
86             server.listenTCP("127.0.0.1:8080", (SocketConnection connection){
87                 // listener 1
88                 // your code here…
89             });
90 
91             server.listenTCP("[::1]:8080", (SocketConnection connection) {
92                 // listener 2
93                 // your code here…
94             });
95         });
96     }
97     ---
98 
99     In practice you might want to use callback variables
100     and reuse them across listeners:
101 
102     ---
103     return runSocketplateApp("my app", args, (SocketServer server)
104     {
105         ConnectionHandler myConnHandler = (SocketConnection connection) {
106             // your code here…
107         };
108 
109         server.listenTCP("127.0.0.1:8080", myConnHandler);
110         server.listenTCP(    "[::1]:8080", myConnHandler);
111     }
112     ---
113 
114     ### Connection usage
115 
116     Established connections are made available as [socketplate.connection.SocketConnection].
117 
118     To close a connection, simply call `.close()` on it.
119 
120     $(TIP
121         Forgetting to close connections is nothing to worry about, though.
122         Socketplate’s workers will close still-alive connctions once a connection handler exits.
123     )
124 
125     #### Sending
126 
127     Sending data is super easy:
128 
129     ---
130     delegate(SocketConnection connection)
131     {
132         ptrdiff_t bytesSent = connection.send("my data");
133     }
134     ---
135 
136     #### Receiving
137 
138     Received data is retrieved via user-provided buffers.
139 
140     ---
141     delegate(SocketConnection connection)
142     {
143         // allocate a new buffer (with a size of 256 bytes)
144         ubyte[] buffer = new ubyte[](256)
145 
146         // read data into the buffer
147         auto bytesReceived = connection.receive(buffer);
148 
149         if (bytesReceived <= 0) {
150             // connection either got closed or timed out,
151             // or an error ocurred
152             return;
153         }
154 
155         // slice the buffer (to view only the received data)
156         ubyte[] data = buffer[0 .. bytesReceived];
157 
158         // do something…
159     }
160     ---
161 
162     ---
163     delegate(SocketConnection connection)
164     {
165         // allocate a new buffer (with a size of 256 bytes)
166         ubyte[] buffer = new ubyte[](256)
167 
168         // read data into the buffer
169         // note: `receiveSlice` throws on error
170         ubyte[] data = connection.receiveSlice(buffer);
171 
172         if (data.length == 0) {
173             // nothing received, connection got closed remotely
174             return;
175         }
176 
177         // do something…
178     }
179     ---
180 
181     ### Logging
182 
183     See [socketplate.log] for details.
184 
185     ---
186     logInfo("My log message");
187     ---
188  +/
189 module socketplate.app;
190 
191 import socketplate.server;
192 import std.format : format;
193 import std.getopt;
194 
195 public import socketplate.address;
196 public import socketplate.connection;
197 public import socketplate.log;
198 public import socketplate.server.server;
199 
200 @safe:
201 
202 /++
203     socketplate quickstart app entry point
204 
205     ---
206     int main(string[] args) {
207         return runSocketplateApp("my app", args, (SocketServer server) {
208             server.listenTCP("127.0.0.1:8080", (SocketConnection connection) {
209                 // IPv4
210             });
211             server.listenTCP("[::1]:8080", (SocketConnection connection) {
212                 // IPv6
213             });
214         });
215     }
216     ---
217 
218     ---
219     int main(string[] args) {
220         return runSocketplateApp("my app", args, (SocketConnection connection) {
221             // listening addresses are read from `args`
222         });
223     }
224     ---
225  +/
226 int runSocketplateApp(
227     string appName,
228     string[] args,
229     void delegate(SocketServer) @safe setupCallback,
230     SocketServerTunables defaults = SocketServerTunables(),
231 )
232 {
233     // “Manual handler setup” mode
234     return runSocketplateAppImpl(appName, args, setupCallback, defaults);
235 }
236 
237 /// ditto
238 int runSocketplateAppTCP(
239     string appName,
240     string[] args,
241     ConnectionHandler tcpConnectionHandler,
242     string[] defaultListeningAddresses = null,
243     SocketServerTunables defaults = SocketServerTunables(),
244 )
245 {
246     // “Single connection handler” mode
247 
248     // This function adds a “listening addresses” parameter `-S` (aka `--serve`) to getopt options.
249     // Also provides a special setup-callback that sets up listeners
250     // for the listening addresses retrieved via getopt.
251     // As fallback, it resorts to `defaultListeningAddresses`.
252 
253     // getopt target
254     string[] sockets;
255 
256     // setup listeners for the requested listening addresses (or default addresses)
257     auto setupCallback = delegate(SocketServer server) @safe {
258         // no listening address requested?
259         if (sockets.length == 0)
260         {
261             // no default address(es) provided?
262             if (defaultListeningAddresses.length == 0)
263             {
264                 logError("No listening addresses specified. Use --serve= ");
265                 return;
266             }
267 
268             // use default address(es) instead
269             foreach (sockAddr; defaultListeningAddresses)
270             {
271                 logTrace("Will listen on default address: " ~ sockAddr);
272                 server.listenTCP(sockAddr, tcpConnectionHandler);
273             }
274 
275             return;
276         }
277 
278         // parse requested listening addresses and register listeners
279         foreach (socket; sockets)
280         {
281             SocketAddress parsed;
282             if (!parseSocketAddress(socket, parsed))
283                 throw new Exception("Invalid listening address: `" ~ socket ~ "`");
284 
285             final switch (parsed.type) with (SocketAddress.Type)
286             {
287             case invalid:
288                 assert(false);
289             case unixDomain:
290                 break;
291             case ipv4:
292             case ipv6:
293                 if (parsed.port <= 0)
294                     throw new Exception(
295                         "Invalid listening address (invalid/missing port): `" ~ socket ~ "`"
296                     );
297                 break;
298             }
299 
300             server.listenTCP(parsed, tcpConnectionHandler);
301         }
302     };
303 
304     return runSocketplateAppImpl(appName, args, setupCallback, defaults, "S|serve", "Socket(s) to listen on", &sockets);
305 }
306 
307 private
308 {
309     int runSocketplateAppImpl(Opts...)(
310         string appName,
311         string[] args,
312         void delegate(SocketServer) @safe setupCallback,
313         SocketServerTunables defaults,
314         Opts opts,
315     )
316     {
317         int workers = int.min;
318         string username = null;
319         string groupname = null;
320         bool verbose = false;
321 
322         // process `args` (usually command line options)
323         GetoptResult getOptR;
324         try
325             getOptR = getopt(
326                 args,
327                 opts,
328                 "w|workers", "Number of workers to start", &workers,
329                 "u|user", "(Privileges dropping) user/uid", &username,
330                 "g|group", "(Privileges dropping) group/gid", &groupname,
331                 "v|verbose", "Enable debug output", &verbose,
332             );
333         catch (GetOptException ex)
334         {
335             import std.stdio : stderr;
336 
337             // bad option (or similar issue)
338             (() @trusted { stderr.writeln(ex.message); })();
339             return 1;
340         }
341 
342         // `--help`?
343         if (getOptR.helpWanted)
344         {
345             defaultGetoptPrinter(appName, getOptR.options);
346             return 0;
347         }
348 
349         // set log level (`--verbose`?)
350         LogLevel logLevel = (verbose) ? LogLevel.trace : LogLevel.info;
351         setLogLevel(logLevel);
352 
353         // apply caller-provided defaults
354         SocketServerTunables tunables = defaults;
355 
356         // apply `--workers` if applicable
357         if (workers != int.min)
358         {
359             if (workers < 1)
360             {
361                 logCritical(format!"Invalid --workers count: %d"(workers));
362                 return 1;
363             }
364 
365             tunables.workers = workers;
366         }
367 
368         // print app name before further setup
369         logInfo(appName);
370         auto server = new SocketServer(tunables);
371 
372         // do setup, if non-null callback provided
373         if (setupCallback !is null)
374         {
375             try
376                 setupCallback(server);
377             catch (Exception ex)
378             {
379                 logTrace("Unhandled exception thrown in setup callback");
380                 logError(ex.msg);
381                 return 1;
382             }
383         }
384 
385         // bind to listening ports
386         server.bind();
387 
388         // drop privileges
389         if (!dropPrivs(username, groupname))
390             return 1;
391 
392         // let’s go
393         return server.run();
394     }
395 
396     bool dropPrivs(string username, string groupname)
397     {
398         import socketplate.privdrop;
399 
400         version (Posix)
401         {
402             auto privilegesDropTo = Privileges();
403 
404             // user specified?
405             if (username !is null)
406             {
407                 uid_t uid;
408                 if (!resolveUsername(username, uid))
409                 {
410                     logCritical(format!"Could not resolve username: %s"(username));
411                     return false;
412                 }
413 
414                 privilegesDropTo.user = uid;
415             }
416 
417             // group specified?
418             if (groupname !is null)
419             {
420                 gid_t gid;
421                 if (!resolveGroupname(groupname, gid))
422                 {
423                     logCritical(format!"Could not resolve groupname: %s"(groupname));
424                     return false;
425                 }
426 
427                 privilegesDropTo.group = gid;
428             }
429 
430             // log applicable target user + group
431             if (!privilegesDropTo.user.isNull)
432                 logInfo(format!"Dropping privileges: uid=%d"(privilegesDropTo.user.get));
433             if (!privilegesDropTo.group.isNull)
434                 logInfo(format!"Dropping privileges: gid=%d"(privilegesDropTo.group.get));
435 
436             // drop privileges
437             if (!dropPrivileges(privilegesDropTo))
438             {
439                 // oh no
440                 logCritical("Dropping privileges failed.");
441                 return false;
442             }
443 
444             // warn if running as “root”
445             Privileges current = currentPrivileges();
446 
447             enum uid_t rootUid = 0;
448             enum gid_t rootGid = 0;
449 
450             if (current.user.get == rootUid)
451                 logWarning("Running as uid=0 (superuser/root)");
452             if (current.group.get == rootGid)
453                 logWarning("Running as gid=0 (superuser/root)");
454 
455             return true;
456         }
457         else
458         {
459             // privilege dropping not implemented on this platform (e.g. on Windows)
460 
461             if (username !is null)
462             {
463                 logCritical("Privilege dropping is not supported (on this platform).");
464                 return false;
465             }
466 
467             if (groupname !is null)
468             {
469                 logCritical("Privilege dropping is not supported (on this platform).");
470                 return false;
471             }
472         }
473     }
474 }