1 /+ 2 MIT License 3 4 Copyright (c) 2020 2night SpA 5 6 Permission is hereby granted, free of charge, to any person obtaining a copy 7 of this software and associated documentation files (the "Software"), to deal 8 in the Software without restriction, including without limitation the rights 9 to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 copies of the Software, and to permit persons to whom the Software is 11 furnished to do so, subject to the following conditions: 12 13 The above copyright notice and this permission notice shall be included in all 14 copies or substantial portions of the Software. 15 16 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 SOFTWARE. 23 +/ 24 25 module dcgi; 26 27 /// UDA for your handler 28 struct MaxRequestBodyLength { size_t length; } 29 30 /// 31 enum DisplayExceptions; 32 33 /// Remember to mixin this template inside your main file. 34 template DCGI(alias handler = null) 35 { 36 void main() 37 { 38 static if (is(typeof(handler) == typeof(null))) 39 { 40 static if (!__traits(compiles, cgi(Request.init, Output.init))) static assert(0, "You must define: void cgi(Request r, Output o) { ... }"); 41 else cgiMain!cgi(); 42 } 43 else 44 { 45 static if (!__traits(compiles, handler(Request.init, Output.init))) static assert(0, "You must define: void " ~ handler.stringof ~ "(Request r, Output o) { ... }"); 46 else cgiMain!handler(); 47 } 48 } 49 } 50 51 /// You're supposed not to call this function. It is called from the mixed-in code 52 void cgiMain(alias handler)() 53 { 54 import std.traits : hasUDA, getUDAs; 55 import std.file : readText; 56 57 // Redirecting stdout to avoid mistakes using writeln & c. 58 import std.stdio : stdout, stderr; 59 auto stream = stdout; 60 stdout = stderr; 61 62 import std.format : format; 63 import std.conv : to; 64 import std.process : environment; 65 import std..string : toUpper; 66 import std.algorithm : map; 67 import std.array : assocArray; 68 import std.typecons : tuple; 69 70 string[string] headers = 71 environment 72 .toAA 73 .byKeyValue 74 .map!(t => tuple(t.key.toUpper,t.value)) 75 .assocArray; 76 77 char[] buffer; 78 79 Request request; 80 Output output; 81 bool showExceptions; 82 size_t maxRequestBodyLength = 1024*4; 83 84 try { 85 output = new Output(stream); 86 showExceptions = hasUDA!(handler, DisplayExceptions); 87 88 static if (hasUDA!(handler, MaxRequestBodyLength)) 89 maxRequestBodyLength = getUDAs!(handler, MaxRequestBodyLength)[0].length; 90 91 { 92 // Read body data 93 import std.stdio : stdin; 94 import core.sys.posix.sys.select; 95 96 // Is there anything on stdin? 97 timeval tv = timeval(0, 1); 98 fd_set fds; 99 FD_ZERO(&fds); 100 FD_SET(0, &fds); 101 select(0+1, &fds, null, null, &tv); 102 103 bool hasData = FD_ISSET(0, &fds); 104 if (hasData) 105 { 106 buffer.length = maxRequestBodyLength + 1; 107 auto requestData = stdin.rawRead(buffer); 108 109 if (requestData.length > maxRequestBodyLength) 110 throw new Exception("Request body too large"); 111 112 buffer.length = requestData.length; 113 } 114 } 115 116 // Init request. Call handler 117 request = new Request(headers, buffer); 118 handler(request, output); 119 } 120 121 // Unhandled Exception escape from user code 122 catch (Exception e) 123 { 124 if (!output.headersSent) 125 output.status = 501; 126 127 cgiLog(format("Unhandled exception: %s", e.msg)); 128 cgiLog(e.to!string); 129 130 if (showExceptions) 131 output(format("<pre>\n%s\n</pre>", e.to!string)); 132 } 133 134 // Even worst. 135 catch (Throwable t) 136 { 137 // I know I'm not supposed to catch Throwable and continue with execution 138 // but it just tries to write exception and exit. 139 140 if (!output.headersSent) 141 output.status = 501; 142 143 cgiLog(format("Throwable: %s", t.msg)); 144 cgiLog(t.to!string); 145 146 if (showExceptions) 147 output(format("<pre>\n%s\n</pre>", t.to!string)); 148 } 149 150 // No reply so far? 151 if (!output.headersSent) 152 { 153 // Send and empty response 154 output(""); 155 } 156 } 157 158 /// Write a formatted log 159 void cgiLog(T...)(T params) 160 { 161 import std.datetime : SysTime, Clock; 162 import std.conv : to; 163 import std.stdio : write, writeln, stderr; 164 165 SysTime t = Clock.currTime; 166 167 stderr.writef( 168 "%04d/%02d/%02d %02d:%02d:%02d.%s >>> ", 169 t.year, t.month, t.day, t.hour,t.minute,t.second,t.fracSecs.split!"msecs".msecs 170 ); 171 172 foreach(p; params) 173 stderr.write(p.to!string, " "); 174 175 stderr.writeln; 176 stderr.flush; 177 } 178 179 /// A cookie 180 private import std.datetime : DateTime; 181 struct Cookie 182 { 183 string name; /// Cookie data 184 string value; /// ditto 185 string path; /// ditto 186 string domain; /// ditto 187 188 DateTime expire; /// ditto 189 190 bool session = true; /// ditto 191 bool secure = false; /// ditto 192 bool httpOnly = false; /// ditto 193 194 /// Invalidate cookie 195 public void invalidate() 196 { 197 expire = DateTime(1970,1,1,0,0,0); 198 } 199 } 200 201 /// A request from user 202 class Request 203 { 204 205 /// HTTP methods 206 public enum Method 207 { 208 Get, /// 209 Post, /// 210 Head, /// 211 Delete, /// 212 Put, /// 213 Unknown = -1 /// 214 } 215 216 @disable this(); 217 218 private this(string[string] headers, char[] requestData) 219 { 220 import std.regex : match, ctRegex; 221 import std.uri : decodeComponent; 222 import std..string : translate, split, toLower; 223 224 // Reset values 225 _header = headers; 226 _get = (typeof(_get)).init; 227 _post = (typeof(_post)).init; 228 _cookie = (typeof(_cookie)).init; 229 _data = requestData; 230 231 // Read get params 232 if ("QUERY_STRING" in _header) 233 foreach(m; match(_header["QUERY_STRING"], ctRegex!("([^=&]+)(?:=([^&]+))?&?", "g"))) 234 _get[m.captures[1].decodeComponent] = translate(m.captures[2], ['+' : ' ']).decodeComponent; 235 236 // Read post params 237 if ("REQUEST_METHOD" in _header && _header["REQUEST_METHOD"] == "POST") 238 if(_data.length > 0 && split(_header["CONTENT_TYPE"].toLower(),";")[0] == "application/x-www-form-urlencoded") 239 foreach(m; match(_data, ctRegex!("([^=&]+)(?:=([^&]+))?&?", "g"))) 240 _post[m.captures[1].decodeComponent] = translate(m.captures[2], ['+' : ' ']).decodeComponent; 241 242 // Read cookies 243 if ("HTTP_COOKIE" in _header) 244 foreach(m; match(_header["HTTP_COOKIE"], ctRegex!("([^=]+)=([^;]+);? ?", "g"))) 245 _cookie[m.captures[1].decodeComponent] = m.captures[2].decodeComponent; 246 247 } 248 249 /// 250 @nogc @property nothrow public const(char[]) data() const { return _data; } 251 252 /// 253 @nogc @property nothrow public const(string[string]) get() const { return _get; } 254 255 /// 256 @nogc @property nothrow public const(string[string]) post() const { return _post; } 257 258 /// 259 @nogc @property nothrow public const(string[string]) header() const { return _header; } 260 261 /// 262 @nogc @property nothrow public const(string[string]) cookie() const { return _cookie; } 263 264 /// 265 @property public Method method() const 266 { 267 import std..string : toLower; 268 switch(_header["REQUEST_METHOD"].toLower()) 269 { 270 case "get": return Method.Get; 271 case "post": return Method.Post; 272 case "head": return Method.Head; 273 case "put": return Method.Put; 274 case "delete": return Method.Delete; 275 default: return Method.Unknown; 276 } 277 } 278 279 private char[] _data; 280 private string[string] _get; 281 private string[string] _post; 282 private string[string] _header; 283 private string[string] _cookie; 284 285 } 286 287 /// Your reply. 288 class Output 289 { 290 private import std.stdio : File; 291 292 private struct KeyValue 293 { 294 this (in string key, in string value) { this.key = key; this.value = value; } 295 string key; 296 string value; 297 } 298 299 /// You can add a http header. But you can't if body is already sent. 300 public void addHeader(in string key, in string value) 301 { 302 if (_headersSent) 303 throw new Exception("Can't add/edit headers. Too late. Just sent."); 304 305 _headers ~= KeyValue(key, value); 306 } 307 308 @disable this(); 309 310 private this(File stream) 311 { 312 _stream = stream; 313 } 314 315 /// Force sending of headers. 316 public void sendHeaders() 317 { 318 import std.format : format; 319 320 if (_headersSent) 321 throw new Exception("Can't resend headers. Too late. Just sent."); 322 323 import std.uri : encode; 324 325 bool has_content_type = false; 326 _stream.write(format("Status: %s\r\n", _status)); 327 328 // send user-defined headers 329 foreach(header; _headers) 330 { 331 import std..string : toLower; 332 _stream.write(format("%s: %s\r\n", header.key, header.value)); 333 if (header.key.toLower() == "content-type") has_content_type = true; 334 } 335 336 // Default content-type is text/html if not defined by user 337 if (!has_content_type) 338 _stream.write(format("Content-Type: text/html; charset=utf-8\r\n")); 339 340 // If required, I add headers to write cookies 341 foreach(Cookie c; _cookies) 342 { 343 344 _stream.write(format("Set-Cookie: %s=%s", c.name.encode(), c.value.encode())); 345 346 if (!c.session) 347 { 348 string[] mm = ["", "jan", "feb", "mar", "apr", "may", "jun", "jul", "aug", "sep", "oct", "nov", "dec"]; 349 string[] dd = ["sun", "mon", "tue", "wed", "thu", "fri", "sat"]; 350 351 string data = format("%s, %s %s %s %s:%s:%s GMT", 352 dd[c.expire.dayOfWeek], c.expire.day, mm[c.expire.month], c.expire.year, 353 c.expire.hour, c.expire.minute, c.expire.second 354 ); 355 356 _stream.write(format("; Expires=%s", data)); 357 } 358 359 if (!c.path.length == 0) _stream.write(format("; path=%s", c.path)); 360 if (!c.domain.length == 0) _stream.write(format("; domain=%s", c.domain)); 361 362 if (c.secure) _stream.write(format("; Secure")); 363 if (c.httpOnly) _stream.write(format("; HttpOnly")); 364 365 _stream.write("\r\n"); 366 } 367 368 _stream.write("\r\n"); 369 _headersSent = true; 370 } 371 372 /// You can set a cookie. But you can't if body is already sent. 373 public void setCookie(Cookie c) 374 { 375 if (_headersSent) 376 throw new Exception("Can't set cookies. Too late. Just sent."); 377 378 _cookies ~= c; 379 } 380 381 /// Retrieve all cookies 382 @nogc @property nothrow public Cookie[] cookies() { return _cookies; } 383 384 /// Output status 385 @nogc @property nothrow public ulong status() { return _status; } 386 387 /// Set response status. Default is 200 (OK) 388 @property public void status(ulong status) 389 { 390 if (_headersSent) 391 throw new Exception("Can't set status. Too late. Just sent."); 392 393 _status = status; 394 } 395 396 /** 397 * Syntax sugar to write data 398 * Example: 399 * -------------------- 400 * output("Hello world", "!"); 401 * -------------------- 402 */ 403 public void opCall(T...)(T params) 404 { 405 import std.conv : to; 406 407 foreach(p; params) 408 write(p); 409 } 410 411 /// Write data 412 public void write(string data) { import std..string : representation; write(data.representation); } 413 414 /// Ditto 415 public void write(in void[] data) 416 { 417 import std.stdio : stdout; 418 419 if (!_headersSent) 420 sendHeaders(); 421 422 _stream.rawWrite(data); 423 } 424 425 /// Are headers already sent? 426 @nogc nothrow public bool headersSent() { return _headersSent; } 427 428 private bool _headersSent = false; 429 private Cookie[] _cookies; 430 private KeyValue[] _headers; 431 private ulong _status = 200; 432 private File _stream; 433 }