1 /** 2 * Support for implementing SCGI application servers. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.net.http.scgi.app; 15 16 import std.algorithm.searching : findSplit; 17 import std.conv : to; 18 import std.exception; 19 import std..string; 20 21 import ae.net.asockets; 22 import ae.net.http.cgi.common; 23 import ae.net.http.cgi.script; 24 import ae.net.http.common; 25 import ae.sys.log; 26 import ae.utils.array; 27 28 /// Implements the SCGI protocol over an abstract connection. 29 final class SCGIConnection 30 { 31 IConnection connection; /// Connection used to construct this object. 32 Logger log; /// Optional logger. 33 bool nph; /// Whether to operate in Non-Parsed Headers mode. 34 35 /// Constructor. 36 /// Params: 37 /// connection = Abstract connection used for communication. 38 this(IConnection connection) 39 { 40 this.connection = connection; 41 connection.handleReadData = &onReadData; 42 } 43 44 private Data buffer; 45 46 protected void onReadData(Data data) 47 { 48 buffer ~= data; 49 50 while (true) 51 try 52 { 53 auto bufferStr = cast(char[])buffer.contents; 54 auto colonIdx = bufferStr.indexOf(':'); 55 if (colonIdx < 0) 56 return; 57 58 auto headerLenStr = bufferStr[0 .. colonIdx]; 59 auto headerLen = headerLenStr.to!size_t; 60 auto headerEnd = headerLenStr.length + 1 /*:*/ + headerLen + 1 /*,*/; 61 if (buffer.length < headerEnd) 62 return; 63 enforce(bufferStr[headerEnd - 1] == ',', "Expected ','"); 64 65 auto headersStr = bufferStr[headerLenStr.length + 1 .. headerEnd - 1]; 66 enum CONTENT_LENGTH = "CONTENT_LENGTH"; 67 enforce(headersStr.startsWith(CONTENT_LENGTH ~ "\0"), "Expected first header to be " ~ CONTENT_LENGTH); 68 auto contentLength = headersStr[CONTENT_LENGTH.length + 1 .. $].findSplit("\0")[0].to!size_t; 69 if (buffer.length < headerEnd + contentLength) 70 return; 71 72 // We now know we have all the data in the request 73 74 auto headers = parseHeaders(headersStr.idup); 75 enforce(headers.get("SCGI", null) == "1", "Unknown SCGI version"); 76 CGIRequest request; 77 request.vars = CGIVars.fromAA(headers); 78 request.headers = CGIRequest.decodeHeaders(headers, request.vars.serverProtocol ? request.vars.serverProtocol : "HTTP"); 79 request.data = DataVec(buffer[headerEnd .. headerEnd + contentLength]); 80 buffer = buffer[headerEnd + contentLength .. $]; 81 handleRequest(request); 82 } 83 catch (Exception e) 84 { 85 if (log) log("Error handling request: " ~ e.toString()); 86 connection.disconnect(e.msg); 87 return; 88 } 89 } 90 91 /// Parse SCGI-formatted headers. 92 static string[string] parseHeaders(string s) 93 { 94 string[string] headers; 95 while (s.length) 96 { 97 auto name = s.skipUntil('\0').enforce("Unterminated header name"); 98 auto value = s.skipUntil('\0').enforce("Unterminated header value"); 99 headers[name] = value; 100 } 101 return headers; 102 } 103 104 /// Write a response. 105 void sendResponse(HttpResponse r) 106 { 107 FastAppender!char headers; 108 if (nph) 109 writeNPHHeaders(r, headers); 110 else 111 writeCGIHeaders(r, headers); 112 connection.send(Data(headers.get)); 113 connection.send(r.data[]); 114 connection.disconnect("Response sent"); 115 } 116 117 /// User-supplied callback for handling incoming requests. 118 void delegate(ref CGIRequest) handleRequest; 119 }