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 <vladimir@thecybershadow.net> 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 struct ServerConfig 44 { 45 enum Transport 46 { 47 inet, 48 unix, 49 stdin, 50 accept, 51 } 52 Transport transport; 53 54 struct Listen 55 { 56 string addr; 57 ushort port; 58 string socketPath; 59 } 60 Listen listen; 61 62 enum Protocol 63 { 64 http, 65 cgi, 66 scgi, 67 fastcgi, 68 } 69 Protocol protocol; 70 71 Nullable!bool nph; 72 73 struct SSL 74 { 75 string cert, key; 76 } 77 SSL ssl; 78 79 string logDir; 80 string prefix = "/"; 81 string username; 82 string password; 83 } 84 85 struct Server(bool useSSL) 86 { 87 static if (useSSL) 88 { 89 import ae.net.ssl.openssl; 90 mixin SSLUseLib; 91 } 92 93 immutable ServerConfig[string] config; 94 95 this(immutable ServerConfig[string] config) 96 { 97 this.config = config; 98 } 99 100 void delegate( 101 HttpRequest request, 102 immutable ref ServerConfig serverConfig, 103 void delegate(HttpResponse) handleResponse, 104 ref Logger log, 105 ) handleRequest; 106 107 string banner; 108 109 void startServer(string serverName) 110 { 111 auto pserverConfig = serverName in config; 112 pserverConfig.enforce(format!"Did not find a server named %s."(serverName)); 113 startServer(serverName, *pserverConfig, true); 114 } 115 116 void startServers() 117 { 118 enforce(config.length, "No servers are configured."); 119 120 foreach (name, serverConfig; config) 121 startServer(name, serverConfig, false); 122 123 enforce(socketManager.size(), "No servers to start!"); 124 socketManager.loop(); 125 } 126 127 bool runImplicitServer() 128 { 129 if (inCGI()) 130 { 131 runImplicitServer( 132 ServerConfig.Transport.stdin, 133 ServerConfig.Protocol.cgi, 134 "cgi", 135 "CGI", 136 "a CGI script"); 137 return true; 138 } 139 140 if (inFastCGI()) 141 { 142 runImplicitServer( 143 ServerConfig.Transport.accept, 144 ServerConfig.Protocol.fastcgi, 145 "fastcgi", 146 "FastCGI", 147 "a FastCGI application"); 148 return true; 149 } 150 151 return false; 152 } 153 154 private: 155 156 void runImplicitServer( 157 ServerConfig.Transport transport, 158 ServerConfig.Protocol protocol, 159 string serverName, 160 string protocolText, 161 string kindText, 162 ) 163 { 164 auto pserverConfig = serverName in config; 165 enum errorFmt = 166 "This program was invoked as %2$s, but no \"%1$s\" server is configured.\n\n" ~ 167 "Please configure a server named %1$s."; 168 enforce(pserverConfig, format!errorFmt(serverName, kindText)); 169 enforce(pserverConfig.transport == transport, 170 format!"The transport must be set to %s in the %s server for implicit %s requests." 171 (transport, serverName, protocolText)); 172 enforce(pserverConfig.protocol == protocol, 173 format!"The protocol must be set to %s in the %s server for implicit %s requests." 174 (protocol, serverName, protocolText)); 175 176 startServer(serverName, *pserverConfig, true); 177 socketManager.loop(); 178 } 179 180 181 void startServer(string name, immutable ServerConfig serverConfig, bool exclusive) 182 { 183 scope(failure) stderr.writefln("Error with server %s:", name); 184 185 auto isSomeCGI = serverConfig.protocol.among( 186 ServerConfig.Protocol.cgi, 187 ServerConfig.Protocol.scgi, 188 ServerConfig.Protocol.fastcgi); 189 190 // Check options 191 if (serverConfig.listen.addr) 192 enforce(serverConfig.transport == ServerConfig.Transport.inet, 193 "listen.addr should only be set with transport = inet"); 194 if (serverConfig.listen.port) 195 enforce(serverConfig.transport == ServerConfig.Transport.inet, 196 "listen.port should only be set with transport = inet"); 197 if (serverConfig.listen.socketPath) 198 enforce(serverConfig.transport == ServerConfig.Transport.unix, 199 "listen.socketPath should only be set with transport = unix"); 200 if (serverConfig.protocol == ServerConfig.Protocol.cgi) 201 enforce(serverConfig.transport == ServerConfig.Transport.stdin, 202 "CGI can only be used with transport = stdin"); 203 if (serverConfig.ssl.cert || serverConfig.ssl.key) 204 enforce(serverConfig.protocol == ServerConfig.Protocol.http, 205 "SSL can only be used with protocol = http"); 206 if (!serverConfig.nph.isNull) 207 enforce(isSomeCGI, 208 "Setting NPH only makes sense with protocol = cgi, scgi, or fastcgi"); 209 enforce(serverConfig.prefix.startsWith("/") && serverConfig.prefix.endsWith("/"), 210 "Server prefix should start and end with /"); 211 212 if (!exclusive && serverConfig.transport.among( 213 ServerConfig.Transport.stdin, 214 ServerConfig.Transport.accept)) 215 { 216 stderr.writefln("Skipping exclusive server %1$s.", name); 217 return; 218 } 219 220 static if (useSSL) SSLContext ctx; 221 if (serverConfig.ssl !is ServerConfig.SSL.init) 222 { 223 static if (useSSL) 224 { 225 ctx = ssl.createContext(SSLContext.Kind.server); 226 ctx.setCertificate(serverConfig.ssl.cert); 227 ctx.setPrivateKey(serverConfig.ssl.key); 228 } 229 else 230 throw new Exception("This executable was built without SSL support. Cannot use SSL, sorry!"); 231 } 232 233 // Place on heap to extend lifetime past scope, 234 // even though this function creates a closure 235 Logger* log = { 236 Logger log; 237 auto logName = "Server-" ~ name; 238 string logDir = serverConfig.logDir; 239 if (logDir is null) 240 logDir = "/dev/stderr"; 241 switch (logDir) 242 { 243 case "/dev/stderr": 244 log = consoleLogger(logName); 245 break; 246 case "/dev/null": 247 log = nullLogger(); 248 break; 249 default: 250 log = fileLogger(logDir ~ "/" ~ logName); 251 break; 252 } 253 return [log].ptr; 254 }(); 255 256 TcpServer server; 257 string protocol = join( 258 (serverConfig.transport == ServerConfig.Transport.inet ? [] : [serverConfig.transport.text]) ~ 259 ( 260 (serverConfig.protocol == ServerConfig.Protocol.http && serverConfig.ssl !is ServerConfig.SSL.init) 261 ? ["https"] 262 : ( 263 [serverConfig.protocol.text] ~ 264 (serverConfig.ssl is ServerConfig.SSL.init ? [] : ["tls"]) 265 ) 266 ), 267 "+"); 268 269 bool nph; 270 if (isSomeCGI) 271 nph = serverConfig.nph.isNull ? isNPH() : serverConfig.nph.get; 272 273 string[] serverAddrs; 274 if (serverConfig.protocol == ServerConfig.Protocol.fastcgi) 275 serverAddrs = environment.get("FCGI_WEB_SERVER_ADDRS", null).split(","); 276 277 void handleConnection(IConnection c, string localAddressStr, string remoteAddressStr) 278 { 279 static if (useSSL) if (ctx) 280 c = ssl.createAdapter(ctx, c); 281 282 void handleRequest(HttpRequest request, void delegate(HttpResponse) handleResponse) 283 { 284 void logAndHandleResponse(HttpResponse response) 285 { 286 log.log([ 287 "", // align IP to tab 288 request ? request.remoteHosts(remoteAddressStr).get(0, remoteAddressStr) : remoteAddressStr, 289 response ? text(cast(ushort)response.status) : "-", 290 request ? format("%9.2f ms", request.age.total!"usecs" / 1000f) : "-", 291 request ? request.method : "-", 292 request ? protocol ~ "://" ~ localAddressStr ~ request.resource : "-", 293 response ? response.headers.get("Content-Type", "-") : "-", 294 request ? request.headers.get("Referer", "-") : "-", 295 request ? request.headers.get("User-Agent", "-") : "-", 296 ].join("\t")); 297 298 handleResponse(response); 299 } 300 301 this.handleRequest(request, serverConfig, &logAndHandleResponse, *log); 302 } 303 304 final switch (serverConfig.protocol) 305 { 306 case ServerConfig.Protocol.cgi: 307 { 308 auto cgiRequest = readCGIRequest(); 309 auto request = new CGIHttpRequest(cgiRequest); 310 bool responseWritten; 311 void handleResponse(HttpResponse response) 312 { 313 if (nph) 314 writeNPHResponse(response); 315 else 316 writeCGIResponse(response); 317 responseWritten = true; 318 } 319 320 handleRequest(request, &handleResponse); 321 assert(responseWritten); 322 break; 323 } 324 case ServerConfig.Protocol.scgi: 325 { 326 auto conn = new SCGIConnection(c); 327 conn.log = *log; 328 conn.nph = nph; 329 void handleSCGIRequest(ref CGIRequest cgiRequest) 330 { 331 auto request = new CGIHttpRequest(cgiRequest); 332 handleRequest(request, &conn.sendResponse); 333 } 334 conn.handleRequest = &handleSCGIRequest; 335 break; 336 } 337 case ServerConfig.Protocol.fastcgi: 338 { 339 if (serverAddrs && !serverAddrs.canFind(remoteAddressStr)) 340 { 341 log.log("Address not in FCGI_WEB_SERVER_ADDRS, rejecting"); 342 c.disconnect("Forbidden by FCGI_WEB_SERVER_ADDRS"); 343 return; 344 } 345 auto fconn = new FastCGIResponderConnection(c); 346 fconn.log = *log; 347 fconn.nph = nph; 348 void handleCGIRequest(ref CGIRequest cgiRequest, void delegate(HttpResponse) handleResponse) 349 { 350 auto request = new CGIHttpRequest(cgiRequest); 351 handleRequest(request, handleResponse); 352 } 353 fconn.handleRequest = &handleCGIRequest; 354 break; 355 } 356 case ServerConfig.Protocol.http: 357 { 358 alias connRemoteAddressStr = remoteAddressStr; 359 alias handleServerRequest = handleRequest; 360 auto self = &this; 361 362 final class HttpConnection : BaseHttpServerConnection 363 { 364 protected: 365 this() 366 { 367 this.log = log; 368 if (self.banner) 369 this.banner = self.banner; 370 this.handleRequest = &onRequest; 371 372 super(c); 373 } 374 375 void onRequest(HttpRequest request) 376 { 377 handleServerRequest(request, &sendResponse); 378 } 379 380 override bool acceptMore() { return server.isListening; } 381 override string formatLocalAddress(HttpRequest r) { return protocol ~ "://" ~ localAddressStr; } 382 override @property string remoteAddressStr() { return connRemoteAddressStr; } 383 } 384 new HttpConnection(); 385 break; 386 } 387 } 388 } 389 390 final switch (serverConfig.transport) 391 { 392 case ServerConfig.Transport.stdin: 393 static if (is(FileConnection)) 394 { 395 import std.stdio : stdin, stdout; 396 import core.sys.posix.unistd : dup; 397 auto c = new Duplex( 398 new FileConnection(stdin.fileno.dup), 399 new FileConnection(stdout.fileno.dup), 400 ); 401 handleConnection(c, 402 environment.get("REMOTE_ADDR", "-"), 403 environment.get("SERVER_NAME", "-")); 404 c.disconnect(); 405 return; 406 } 407 else 408 throw new Exception("Sorry, transport = stdin is not supported on this platform!"); 409 case ServerConfig.Transport.accept: 410 server = TcpServer.fromStdin(); 411 break; 412 case ServerConfig.Transport.inet: 413 server = new TcpServer(); 414 server.listen(serverConfig.listen.port, serverConfig.listen.addr); 415 break; 416 case ServerConfig.Transport.unix: 417 { 418 server = new TcpServer(); 419 static if (is(UnixAddress)) 420 { 421 string socketPath = serverConfig.listen.socketPath; 422 // Work around "path too long" errors with long $PWD 423 { 424 import std.path : relativePath; 425 auto relPath = relativePath(socketPath); 426 if (relPath.length < socketPath.length) 427 socketPath = relPath; 428 } 429 socketPath.remove().collectException(); 430 431 AddressInfo ai; 432 ai.family = AddressFamily.UNIX; 433 ai.type = SocketType.STREAM; 434 ai.address = new UnixAddress(socketPath); 435 server.listen([ai]); 436 437 addShutdownHandler({ socketPath.remove(); }); 438 } 439 else 440 throw new Exception("UNIX sockets are not available on this platform"); 441 } 442 } 443 444 addShutdownHandler({ server.close(); }); 445 446 server.handleAccept = 447 (TcpConnection incoming) 448 { 449 handleConnection(incoming, incoming.localAddressStr, incoming.remoteAddressStr); 450 }; 451 452 foreach (address; server.localAddresses) 453 log.log("Listening on " ~ formatAddress(protocol, address) ~ " [" ~ to!string(address.addressFamily) ~ "]"); 454 } 455 } 456 457 unittest 458 { 459 Server!false testServer; 460 }