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.dataset : DataVec; 26 import ae.sys.log; 27 import ae.utils.array; 28 29 /// Implements the SCGI protocol over an abstract connection. 30 final class SCGIConnection 31 { 32 IConnection connection; /// Connection used to construct this object. 33 Logger log; /// Optional logger. 34 bool nph; /// Whether to operate in Non-Parsed Headers mode. 35 36 /// Constructor. 37 /// Params: 38 /// connection = Abstract connection used for communication. 39 this(IConnection connection) 40 { 41 this.connection = connection; 42 connection.handleReadData = &onReadData; 43 } 44 45 private Data buffer; 46 47 protected void onReadData(Data data) 48 { 49 buffer ~= data; 50 51 while (true) 52 try 53 { 54 buffer.asDataOf!char.enter((scope bufferStr) { 55 auto colonIdx = bufferStr.indexOf(':'); 56 if (colonIdx < 0) 57 return; 58 59 auto headerLenStr = bufferStr[0 .. colonIdx]; 60 auto headerLen = headerLenStr.to!size_t; 61 auto headerEnd = headerLenStr.length + 1 /*:*/ + headerLen + 1 /*,*/; 62 if (buffer.length < headerEnd) 63 return; 64 enforce(bufferStr[headerEnd - 1] == ',', "Expected ','"); 65 66 auto headersStr = bufferStr[headerLenStr.length + 1 .. headerEnd - 1]; 67 enum CONTENT_LENGTH = "CONTENT_LENGTH"; 68 enforce(headersStr.startsWith(CONTENT_LENGTH ~ "\0"), "Expected first header to be " ~ CONTENT_LENGTH); 69 auto contentLength = headersStr[CONTENT_LENGTH.length + 1 .. $].findSplit("\0")[0].to!size_t; 70 if (buffer.length < headerEnd + contentLength) 71 return; 72 73 // We now know we have all the data in the request 74 75 auto headers = parseHeaders(headersStr.idup); 76 enforce(headers.get("SCGI", null) == "1", "Unknown SCGI version"); 77 CGIRequest request; 78 request.vars = CGIVars.fromAA(headers); 79 request.headers = CGIRequest.decodeHeaders(headers, request.vars.serverProtocol ? request.vars.serverProtocol : "HTTP"); 80 request.data = DataVec(buffer[headerEnd .. headerEnd + contentLength]); 81 buffer = buffer[headerEnd + contentLength .. $]; 82 handleRequest(request); 83 }); 84 } 85 catch (Exception e) 86 { 87 if (log) log("Error handling request: " ~ e.toString()); 88 connection.disconnect(e.msg); 89 return; 90 } 91 } 92 93 /// Parse SCGI-formatted headers. 94 static string[string] parseHeaders(string s) 95 { 96 string[string] headers; 97 while (s.length) 98 { 99 auto name = s.skipUntil('\0').enforce("Unterminated header name"); 100 auto value = s.skipUntil('\0').enforce("Unterminated header value"); 101 headers[name] = value; 102 } 103 return headers; 104 } 105 106 /// Write a response. 107 void sendResponse(HttpResponse r) 108 { 109 FastAppender!char headers; 110 if (nph) 111 writeNPHHeaders(r, headers); 112 else 113 writeCGIHeaders(r, headers); 114 connection.send(Data(headers.get().asBytes)); 115 connection.send(r.data[]); 116 connection.disconnect("Response sent"); 117 } 118 119 /// User-supplied callback for handling incoming requests. 120 void delegate(ref CGIRequest) handleRequest; 121 }