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 }