1 /** 2 * Flexible app server glue, supporting all 3 * protocols implemented in this library. 4 * 5 * License: 6 * This Source Code Form is subject to the terms of 7 * the Mozilla Public License, v. 2.0. If a copy of 8 * the MPL was not distributed with this file, You 9 * can obtain one at http://mozilla.org/MPL/2.0/. 10 * 11 * Authors: 12 * Vladimir Panteleev <ae@cy.md> 13 */ 14 15 module ae.net.http.app.server; 16 17 debug version(unittest) version = SSL; 18 19 import std.algorithm.comparison; 20 import std.algorithm.searching; 21 import std.array; 22 import std.conv; 23 import std.exception; 24 import std.file; 25 import std.format; 26 import std.process : environment; 27 import std.socket; 28 import std.stdio : stderr; 29 import std.typecons; 30 31 import ae.net.asockets; 32 import ae.net.http.cgi.common; 33 import ae.net.http.cgi.script; 34 import ae.net.http.fastcgi.app; 35 import ae.net.http.responseex; 36 import ae.net.http.scgi.app; 37 import ae.net.http.server; 38 import ae.net.shutdown; 39 import ae.net.ssl; 40 import ae.sys.log; 41 import ae.utils.array; 42 43 /// Describes one configuration for how a listener can receive 44 /// HTTP requests and send responses. 45 struct ServerConfig 46 { 47 /// Low-level transport to listen on. 48 enum Transport 49 { 50 inet, /// Internet Protocol (TCP) 51 unix, /// UNIX socket 52 stdin, /// Standard input (like CGI) 53 accept, /// A socket that can be accepted on standard input (like FastCGI) 54 } 55 Transport transport; /// ditto 56 57 /// Listen parameters 58 struct Listen 59 { 60 /// Local address to bind to 61 string addr; 62 /// Port number to listen on 63 ushort port; 64 /// Path to UNIX socket to listen on 65 string socketPath; 66 } 67 Listen listen; /// ditto 68 69 /// Protocol in which requests arrive and responses should be sent in 70 enum Protocol 71 { 72 http, /// Standard HTTP 73 cgi, /// CGI 74 scgi, /// SCGI 75 fastcgi, /// FastCGI 76 } 77 Protocol protocol; /// ditto 78 79 /// Whether "No Parsed Headers" mode is enabled or not (null = autodetect). 80 Nullable!bool nph; 81 82 /// SSL parameters. If set, enables TLS. 83 struct SSL 84 { 85 /// Path to a PEM-encoded file containing the public part of the certificate. 86 string cert; 87 /// Path to a PEM-encoded file containing the certificate's private key. 88 string key; 89 } 90 SSL ssl; /// ditto 91 92 string logDir; /// Directory where logs are to be saved. If null, just write to stderr. 93 string prefix = "/"; /// URL prefix that requests are expected to start with. 94 string username; /// Require this username via HTTP basic authentication, if set. 95 string password; /// Require this password via HTTP basic authentication, if set. 96 } 97 98 /// An object which receives HTTP requests through one or more 99 /// channels and sends replies according to a user-supplied handler. 100 /// Params: 101 /// useSSL = Whether to compile SSL support (using ae.net.ssl). 102 struct Server(bool useSSL) 103 { 104 static if (useSSL) 105 { 106 static import ae.net.ssl.openssl; 107 mixin ae.net.ssl.openssl.SSLUseLib; 108 } 109 110 /// Configuration (as specified when constructing). 111 immutable ServerConfig[string] config; 112 113 /// Constructor. 114 /// Params: 115 /// config = An associative array from listener names to their 116 /// listen configuration. 117 this(immutable ServerConfig[string] config) 118 { 119 this.config = config; 120 } 121 122 /// The use-specified request handler. 123 /// Params: 124 /// request = The HTTP request object to respond to. 125 /// serverConfig = The configuration of the listener that 126 /// received this request. 127 /// handleResponse = Call this to send a response. 128 /// log = The Logger object associated with this 129 /// server. Can be used to log more 130 /// information. 131 void delegate( 132 HttpRequest request, 133 immutable ref ServerConfig serverConfig, 134 void delegate(HttpResponse) handleResponse, 135 ref Logger log, 136 ) handleRequest; 137 138 /// Send this as the X-Powered-By header. 139 string banner; 140 141 /// Start only the server with the specified name. 142 /// Does not start an event loop, and returns immediately. 143 void startServer(string serverName) 144 { 145 auto pserverConfig = serverName in config; 146 pserverConfig.enforce(format!"Did not find a server named %s."(serverName)); 147 startServer(serverName, *pserverConfig, true); 148 } 149 150 /// Start all configured servers, and runs an event loop. 151 /// Shutdown handlers will be registered, so calling 152 /// `ae.net.shutdown.shutdown` will stop all servers. 153 void startServers() 154 { 155 enforce(config.length, "No servers are configured."); 156 157 foreach (name, serverConfig; config) 158 startServer(name, serverConfig, false); 159 160 enforce(socketManager.size(), "No servers to start!"); 161 socketManager.loop(); 162 } 163 164 /// If the current process is being run in a certain environment 165 /// that implies a particular way of handling requests, run an 166 /// appropriate server until termination and return true. 167 /// Otherwise, return false. 168 bool runImplicitServer() 169 { 170 if (inCGI()) 171 { 172 runImplicitServer( 173 ServerConfig.Transport.stdin, 174 ServerConfig.Protocol.cgi, 175 "cgi", 176 "CGI", 177 "a CGI script"); 178 return true; 179 } 180 181 if (inFastCGI()) 182 { 183 runImplicitServer( 184 ServerConfig.Transport.accept, 185 ServerConfig.Protocol.fastcgi, 186 "fastcgi", 187 "FastCGI", 188 "a FastCGI application"); 189 return true; 190 } 191 192 return false; 193 } 194 195 private: 196 197 void runImplicitServer( 198 ServerConfig.Transport transport, 199 ServerConfig.Protocol protocol, 200 string serverName, 201 string protocolText, 202 string kindText, 203 ) 204 { 205 auto pserverConfig = serverName in config; 206 enum errorFmt = 207 "This program was invoked as %2$s, but no \"%1$s\" server is configured.\n\n" ~ 208 "Please configure a server named %1$s."; 209 enforce(pserverConfig, format!errorFmt(serverName, kindText)); 210 enforce(pserverConfig.transport == transport, 211 format!"The transport must be set to %s in the %s server for implicit %s requests." 212 (transport, serverName, protocolText)); 213 enforce(pserverConfig.protocol == protocol, 214 format!"The protocol must be set to %s in the %s server for implicit %s requests." 215 (protocol, serverName, protocolText)); 216 217 startServer(serverName, *pserverConfig, true); 218 socketManager.loop(); 219 } 220 221 222 void startServer(string name, immutable ServerConfig serverConfig, bool exclusive) 223 { 224 scope(failure) stderr.writefln("Error with server %s:", name); 225 226 auto isSomeCGI = serverConfig.protocol.among( 227 ServerConfig.Protocol.cgi, 228 ServerConfig.Protocol.scgi, 229 ServerConfig.Protocol.fastcgi); 230 231 // Check options 232 if (serverConfig.listen.addr) 233 enforce(serverConfig.transport == ServerConfig.Transport.inet, 234 "listen.addr should only be set with transport = inet"); 235 if (serverConfig.listen.port) 236 enforce(serverConfig.transport == ServerConfig.Transport.inet, 237 "listen.port should only be set with transport = inet"); 238 if (serverConfig.listen.socketPath) 239 enforce(serverConfig.transport == ServerConfig.Transport.unix, 240 "listen.socketPath should only be set with transport = unix"); 241 if (serverConfig.protocol == ServerConfig.Protocol.cgi) 242 enforce(serverConfig.transport == ServerConfig.Transport.stdin, 243 "CGI can only be used with transport = stdin"); 244 if (serverConfig.ssl.cert || serverConfig.ssl.key) 245 enforce(serverConfig.protocol == ServerConfig.Protocol.http, 246 "SSL can only be used with protocol = http"); 247 if (!serverConfig.nph.isNull) 248 enforce(isSomeCGI, 249 "Setting NPH only makes sense with protocol = cgi, scgi, or fastcgi"); 250 enforce(serverConfig.prefix.startsWith("/") && serverConfig.prefix.endsWith("/"), 251 "Server prefix should start and end with /"); 252 253 if (!exclusive && serverConfig.transport.among( 254 ServerConfig.Transport.stdin, 255 ServerConfig.Transport.accept)) 256 { 257 stderr.writefln("Skipping exclusive server %1$s.", name); 258 return; 259 } 260 261 static if (useSSL) SSLContext ctx; 262 if (serverConfig.ssl !is ServerConfig.SSL.init) 263 { 264 static if (useSSL) 265 { 266 ctx = ssl.createContext(SSLContext.Kind.server); 267 ctx.setCertificate(serverConfig.ssl.cert); 268 ctx.setPrivateKey(serverConfig.ssl.key); 269 } 270 else 271 throw new Exception("This executable was built without SSL support. Cannot use SSL, sorry!"); 272 } 273 274 // Place on heap to extend lifetime past scope, 275 // even though this function creates a closure 276 Logger* log = { 277 Logger log; 278 auto logName = "Server-" ~ name; 279 string logDir = serverConfig.logDir; 280 if (logDir is null) 281 logDir = "/dev/stderr"; 282 switch (logDir) 283 { 284 case "/dev/stderr": 285 log = consoleLogger(logName); 286 break; 287 case "/dev/null": 288 log = nullLogger(); 289 break; 290 default: 291 log = fileLogger(logDir ~ "/" ~ logName); 292 break; 293 } 294 return [log].ptr; 295 }(); 296 297 SocketServer server; 298 string protocol = join( 299 (serverConfig.transport == ServerConfig.Transport.inet ? [] : [serverConfig.transport.text]) ~ 300 ( 301 (serverConfig.protocol == ServerConfig.Protocol.http && serverConfig.ssl !is ServerConfig.SSL.init) 302 ? ["https"] 303 : ( 304 [serverConfig.protocol.text] ~ 305 (serverConfig.ssl is ServerConfig.SSL.init ? [] : ["tls"]) 306 ) 307 ), 308 "+"); 309 310 bool nph; 311 if (isSomeCGI) 312 nph = serverConfig.nph.isNull ? isNPH() : serverConfig.nph.get; 313 314 string[] serverAddrs; 315 if (serverConfig.protocol == ServerConfig.Protocol.fastcgi) 316 serverAddrs = environment.get("FCGI_WEB_SERVER_ADDRS", null).split(","); 317 318 void handleConnection(IConnection c, string localAddressStr, string remoteAddressStr) 319 { 320 static if (useSSL) if (ctx) 321 c = ssl.createAdapter(ctx, c); 322 323 void handleRequest(HttpRequest request, void delegate(HttpResponse) handleResponse) 324 { 325 void logAndHandleResponse(HttpResponse response) 326 { 327 log.log([ 328 "", // align IP to tab 329 remoteAddressStr, 330 response ? text(cast(ushort)response.status) : "-", 331 request ? format("%9.2f ms", request.age.total!"usecs" / 1000f) : "-", 332 request ? request.method : "-", 333 request ? protocol ~ "://" ~ localAddressStr ~ request.resource : "-", 334 response ? response.headers.get("Content-Type", "-") : "-", 335 request ? request.headers.get("Referer", "-") : "-", 336 request ? request.headers.get("User-Agent", "-") : "-", 337 ].join("\t")); 338 339 handleResponse(response); 340 } 341 342 this.handleRequest(request, serverConfig, &logAndHandleResponse, *log); 343 } 344 345 final switch (serverConfig.protocol) 346 { 347 case ServerConfig.Protocol.cgi: 348 { 349 auto cgiRequest = readCGIRequest(); 350 auto request = new CGIHttpRequest(cgiRequest); 351 bool responseWritten; 352 void handleResponse(HttpResponse response) 353 { 354 if (nph) 355 writeNPHResponse(response); 356 else 357 writeCGIResponse(response); 358 responseWritten = true; 359 } 360 361 handleRequest(request, &handleResponse); 362 assert(responseWritten); 363 break; 364 } 365 case ServerConfig.Protocol.scgi: 366 { 367 auto conn = new SCGIConnection(c); 368 conn.log = *log; 369 conn.nph = nph; 370 void handleSCGIRequest(ref CGIRequest cgiRequest) 371 { 372 auto request = new CGIHttpRequest(cgiRequest); 373 handleRequest(request, &conn.sendResponse); 374 } 375 conn.handleRequest = &handleSCGIRequest; 376 break; 377 } 378 case ServerConfig.Protocol.fastcgi: 379 { 380 if (serverAddrs && !serverAddrs.canFind(remoteAddressStr)) 381 { 382 log.log("Address not in FCGI_WEB_SERVER_ADDRS, rejecting"); 383 c.disconnect("Forbidden by FCGI_WEB_SERVER_ADDRS"); 384 return; 385 } 386 auto fconn = new FastCGIResponderConnection(c); 387 fconn.log = *log; 388 fconn.nph = nph; 389 void handleCGIRequest(ref CGIRequest cgiRequest, void delegate(HttpResponse) handleResponse) 390 { 391 auto request = new CGIHttpRequest(cgiRequest); 392 handleRequest(request, handleResponse); 393 } 394 fconn.handleRequest = &handleCGIRequest; 395 break; 396 } 397 case ServerConfig.Protocol.http: 398 { 399 alias connRemoteAddressStr = remoteAddressStr; 400 alias handleServerRequest = handleRequest; 401 auto self = &this; 402 403 final class HttpConnection : BaseHttpServerConnection 404 { 405 protected: 406 this() 407 { 408 this.log = log; 409 if (self.banner) 410 this.banner = self.banner; 411 this.handleRequest = &onRequest; 412 413 super(c); 414 } 415 416 void onRequest(HttpRequest request) 417 { 418 handleServerRequest(request, &sendResponse); 419 } 420 421 override bool acceptMore() { return server.isListening; } 422 override string formatLocalAddress(HttpRequest r) { return protocol ~ "://" ~ localAddressStr; } 423 override @property string remoteAddressStr(HttpRequest r) { return connRemoteAddressStr; } 424 } 425 new HttpConnection(); 426 break; 427 } 428 } 429 } 430 431 final switch (serverConfig.transport) 432 { 433 case ServerConfig.Transport.stdin: 434 static if (is(FileConnection)) 435 { 436 import std.stdio : stdin, stdout; 437 import core.sys.posix.unistd : dup; 438 auto c = new Duplex( 439 new FileConnection(stdin.fileno.dup), 440 new FileConnection(stdout.fileno.dup), 441 ); 442 handleConnection(c, 443 environment.get("REMOTE_ADDR", "-"), 444 environment.get("SERVER_NAME", "-")); 445 c.disconnect(); 446 return; 447 } 448 else 449 throw new Exception("Sorry, transport = stdin is not supported on this platform!"); 450 case ServerConfig.Transport.accept: 451 server = SocketServer.fromStdin(); 452 break; 453 case ServerConfig.Transport.inet: 454 { 455 auto tcpServer = new TcpServer(); 456 tcpServer.listen(serverConfig.listen.port, serverConfig.listen.addr); 457 server = tcpServer; 458 break; 459 } 460 case ServerConfig.Transport.unix: 461 { 462 server = new SocketServer(); 463 static if (is(UnixAddress)) 464 { 465 string socketPath = serverConfig.listen.socketPath; 466 // Work around "path too long" errors with long $PWD 467 { 468 import std.path : relativePath; 469 auto relPath = relativePath(socketPath); 470 if (relPath.length < socketPath.length) 471 socketPath = relPath; 472 } 473 socketPath.remove().collectException(); 474 475 AddressInfo ai; 476 ai.family = AddressFamily.UNIX; 477 ai.type = SocketType.STREAM; 478 ai.address = new UnixAddress(socketPath); 479 server.listen([ai]); 480 481 addShutdownHandler((scope const(char)[] /*reason*/) { socketPath.remove(); }); 482 } 483 else 484 throw new Exception("UNIX sockets are not available on this platform"); 485 } 486 } 487 488 addShutdownHandler((scope const(char)[] /*reason*/) { server.close(); }); 489 490 server.handleAccept = 491 (SocketConnection incoming) 492 { 493 handleConnection(incoming, incoming.localAddressStr, incoming.remoteAddressStr); 494 }; 495 496 foreach (address; server.localAddresses) 497 log.log("Listening on " ~ formatAddress(protocol, address) ~ " [" ~ to!string(address.addressFamily) ~ "]"); 498 } 499 } 500 501 unittest 502 { 503 alias Test = Server!false; 504 }