1 module more.cgi; 2 3 static import core.stdc.stdio; 4 import core.stdc..string : strlen, strcpy, memmove; 5 import core.time : MonoTime, Duration; 6 import core.stdc.stdlib : alloca; 7 8 import std.typecons : Flag, Yes, No; 9 import std.format : format, sformat; 10 import std..string : startsWith, indexOf; 11 import std.algorithm : canFind; 12 import std.conv : text, to; 13 import std.traits : hasMember; 14 import std.typecons : tuple; 15 import std.random : Random; 16 import std.exception : ErrnoException; 17 import std.stdio : File, stdout, stderr, stdin; 18 19 import more.parse : hexValue, findCharPtr, skipCharSet; 20 import more.format : formatEscapeByPolicy, formatEscapeSet, asciiFormatEscaped; 21 public import more.format : formatHex; 22 public import more.uri : uriDecode, tryUriDecode; 23 24 version = ToLogFile; 25 version (ToLogFile) 26 { 27 private __gshared bool logFileOpen = false; 28 private __gshared File logFile; 29 } 30 31 void log(T...)(T args) 32 { 33 stderr.writeln(args); 34 stderr.flush(); 35 } 36 void logf(T...)(const(char)[] fmt, T args) 37 { 38 version (ToLogFile) 39 { 40 if (!logFileOpen) 41 { 42 logFile = File("/tmp/log", "a"); 43 import std.datetime; 44 logFile.writefln("---------------- %s -----------------------", Clock.currTime); 45 logFileOpen = true; 46 } 47 logFile.writefln(fmt, args); 48 logFile.flush(); 49 } 50 stderr.writefln(fmt, args); 51 stderr.flush(); 52 } 53 54 auto fread(T)(ref File file, T[] buffer) 55 { 56 return core.stdc.stdio.fread(buffer.ptr, T.sizeof, buffer.length, file.getFP); 57 } 58 59 enum ResponseState 60 { 61 headers, content 62 } 63 __gshared ResponseState responseState = ResponseState.headers; 64 void ensureResponseFinished() 65 { 66 if(responseState == ResponseState.headers) { 67 finishHeaders(); 68 } 69 } 70 71 bool inHeaders() 72 { 73 return responseState == ResponseState.headers; 74 } 75 76 void assertInHeaders(string functionName) 77 { 78 if(responseState != ResponseState.headers) 79 { 80 logf("Error: function \"%s\" was called after headers were finished", functionName); 81 assert(0, "function \"" ~ functionName ~ "\" was called after headers were finished"); 82 } 83 } 84 void assertInContent(string functionName) 85 { 86 version(unittest) 87 { 88 return; 89 } 90 if(responseState != ResponseState.content) 91 { 92 logf("Error: function \"%s\" was called before headers were finished", functionName); 93 assert(0, "function \"" ~ functionName ~ "\" was called before headers were finished"); 94 } 95 } 96 97 @property bool inHeaderState() { return responseState == ResponseState.headers; } 98 void addHeader(const(char)[] name, const(char)[] value) 99 { 100 assertInHeaders("addHeader"); 101 stdout.writef("%s: %s\r\n", name, value); 102 } 103 void addSetCookieHeader(const(char)[] name, const(char)[] value, Duration expireTimeFromNow = Duration.zero) 104 { 105 assertInHeaders("addSetCookieHeader"); 106 if(expireTimeFromNow == Duration.zero) { 107 stdout.writef("Set-Cookie: %s=%s\r\n", name, value); 108 } else { 109 assert(0, "not implemented"); 110 } 111 } 112 void addUnsetCookieHeader(const(char)[] name) 113 { 114 assertInHeaders("addUnsetCookieHeader"); 115 stdout.writef("Set-Cookie: %s=; expires=Thu, 01 Jan 1970 00:00:00 GMT\r\n", name); 116 } 117 void finishHeaders() 118 { 119 assertInHeaders("finishHeaders"); 120 stdout.write("\r\n"); 121 responseState = ResponseState.content; 122 } 123 124 // writes a string or formatted string to stdout 125 void write(T...)(const(char)[] fmt, T args) 126 { 127 assertInContent("write"); 128 static if(T.length == 0) { 129 stdout.write(fmt); 130 } else { 131 stdout.writef(fmt, args); 132 } 133 } 134 // with an appended '\n' character, writes a string or formatted 135 // string to stdout 136 void writeln(T...)(const(char)[] fmt, T args) 137 { 138 assertInContent("writeln"); 139 static if(T.length == 0) { 140 stdout.writeln(fmt); 141 } else { 142 stdout.writefln(fmt, args); 143 } 144 } 145 // writes a list of values to stdout 146 void listWrite(T...)(T args) 147 { 148 assertInContent("listWrite"); 149 stdout.write(args); 150 } 151 // with an appended '\n' character, writes a list of values to stdout 152 void listWriteln(T...)(T args) 153 { 154 assertInContent("listWriteln"); 155 stdout.writeln(args); 156 } 157 unittest 158 { 159 import more.test; 160 mixin(scopedTest!"cgi - Hello World"); 161 162 // just instantiate the templates 163 // all of the following should print "Hello, World!\n" 164 if (false) 165 { 166 write("Hello"); 167 write(", World!\n"); 168 write("Hello%s", ", World!\n"); 169 writeln("Hello, World!"); 170 writeln("Hello%s", ", World!"); 171 listWrite("Hello", ", Wo", "rld!\n"); 172 listWriteln("Hel", "lo, Wo", "rld!"); 173 } 174 } 175 176 void writeFile(const(char)[] filename) 177 { 178 assertInContent("writeFile"); 179 auto file = File(filename, "rb"); 180 181 enum MAX_BUFFER_SIZE = 8192; 182 auto fileSize = file.size(); 183 auto bufferSize = (fileSize > MAX_BUFFER_SIZE) ? MAX_BUFFER_SIZE : cast(size_t) fileSize; 184 auto buffer = new char[bufferSize]; 185 auto fileLeft = fileSize; 186 for(;fileLeft > 0;) 187 { 188 auto nextReadSize = (fileLeft > bufferSize) ? bufferSize : cast(size_t)fileLeft; 189 auto readSize = fread(file, buffer); 190 if(readSize == 0) 191 { 192 auto message = format("only read %s bytes out of %s byte file \"%s\"", fileSize - fileLeft, fileSize, filename); 193 logf("Error: %s", message); 194 assert(0, message); 195 } 196 stdout.rawWrite(buffer[0..readSize]); 197 fileLeft -= readSize; 198 } 199 } 200 201 auto dupzstring(T)(T[] str) 202 { 203 auto result = str.ptr[0..str.length + 1].dup; 204 return result[0..$-1]; 205 } 206 207 version(Windows) 208 { 209 struct EnvPairString 210 { 211 private const(char)[] pair; 212 //@property const(char)* ptr() { return pair.ptr; } 213 auto asDString() { return pair; } 214 } 215 import core.sys.windows.windows : GetLastError, GetEnvironmentStringsA, FreeEnvironmentStringsA; 216 private struct EnvPairStringsRange 217 { 218 const(char)* environmentStrings; 219 EnvPairString current; 220 this(const(char)* environmentStrings) 221 { 222 this.environmentStrings = environmentStrings; 223 this.current.pair = environmentStrings[0..strlen(environmentStrings)]; 224 } 225 @property bool empty() { return current.pair.ptr == null; } 226 auto front() { return current; } 227 void popFront() 228 { 229 auto nextPtr = current.pair.ptr + current.pair.length + 1; 230 if(*nextPtr == '\0') 231 { 232 current.pair = null; 233 assert(FreeEnvironmentStringsA(cast(char*)environmentStrings)); 234 } 235 else 236 { 237 current.pair = nextPtr[0..strlen(nextPtr)]; 238 } 239 } 240 } 241 auto envPairStrings() 242 { 243 auto environmentStrings = GetEnvironmentStringsA(); 244 assert(environmentStrings, format("GetEnvironmentStringsA failed (e=%s)", GetLastError())); 245 return EnvPairStringsRange(environmentStrings); 246 } 247 struct EnvPair 248 { 249 private const(char)[] name; 250 private const(char)[] value; 251 @property const(char)[] tempName() { return name; } 252 @property const(char)[] tempValue() { return value; } 253 @property char[] permanentValue() { return value.dupzstring; } 254 } 255 auto envPairs() 256 { 257 struct Range 258 { 259 EnvPairStringsRange range; 260 @property bool empty() { return range.empty(); } 261 auto front() 262 { 263 auto pairString = range.front(); 264 auto nameLimit = pairString.pair.ptr.findCharPtr('='); 265 auto nameLimitIndex = nameLimit - pairString.pair.ptr; 266 auto valueStartIndex = nameLimitIndex + ( (*nameLimit == '=') ? 1 : 0); 267 268 return EnvPair(pairString.pair[ 0 .. nameLimitIndex], 269 pairString.pair[valueStartIndex .. $ ]); 270 } 271 void popFront() 272 { 273 range.popFront(); 274 } 275 } 276 return Range(envPairStrings()); 277 } 278 } 279 else 280 { 281 struct EnvPairString 282 { 283 private char* pair; 284 //@property const(char)* ptr() { return pair; } 285 auto asDString() { return pair[0..strlen(pair)]; } 286 } 287 288 extern(C) extern __gshared char** environ; 289 private struct EnvPairStringsRange 290 { 291 EnvPairString* next; 292 @property bool empty() { return next.pair is null; } 293 auto front() { return *next; } 294 void popFront() 295 { 296 next++; 297 } 298 } 299 auto envPairStrings() 300 { 301 return EnvPairStringsRange(cast(EnvPairString*)environ); 302 } 303 struct EnvPair 304 { 305 private char[] name; 306 private char[] value; 307 @property char[] tempName() { return name; } 308 @property char[] tempValue() { return value; } 309 @property char[] permanentValue() { return value; } 310 } 311 auto envPairs() 312 { 313 struct Range 314 { 315 EnvPairStringsRange range; 316 @property bool empty() { return range.empty(); } 317 auto front() 318 { 319 auto pairString = range.front(); 320 auto nameLimit = pairString.pair.findCharPtr('='); 321 auto nameLimitIndex = nameLimit - pairString.pair; 322 auto valueStart = nameLimit + ( (*nameLimit == '=') ? 1 : 0); 323 324 return EnvPair(pairString.pair[ 0 .. nameLimitIndex], 325 valueStart[0..strlen(valueStart)]); 326 } 327 void popFront() 328 { 329 range.popFront(); 330 } 331 } 332 return Range(envPairStrings()); 333 } 334 } 335 336 auto cookieRange(T)(T* cookieString) 337 { 338 struct Cookie 339 { 340 T[] name; 341 T[] value; 342 } 343 struct Range 344 { 345 Cookie cookie; 346 T* next; 347 this(T* next) 348 { 349 this.next = next; 350 popFront(); 351 } 352 @property bool empty() { return cookie.name is null; } 353 auto front() { return cookie; } 354 void popFront() 355 { 356 //logf("cookieRange.popFront (\"%s\")", next[0..strlen(next)]); 357 next = skipCharSet!" ;"(next); 358 if(*next == '\0') { 359 cookie.name = null; 360 //logf("cookieRange.popFront return null"); 361 return; 362 } 363 364 auto namePtr = next; 365 auto valuePtr = findCharPtr(next, '='); 366 cookie.name = namePtr[0..valuePtr - namePtr]; 367 trimRight(&cookie.name); 368 if(*valuePtr == '=') { 369 valuePtr++; 370 next = findCharPtr(next, ';'); 371 cookie.value = valuePtr[0..next - valuePtr]; 372 trimRight(&cookie.value); 373 } else { 374 cookie.value = valuePtr[0..0]; 375 next = valuePtr; 376 } 377 //logf("cookieRange.popFront name=\"%s\" value=\"%s\"", cookie.name, cookie.value); 378 } 379 } 380 return Range(cookieString); 381 } 382 383 auto queryVarsRange(T)(T* varZString) 384 { 385 struct Var 386 { 387 T[] name; 388 T[] value; 389 } 390 struct Range 391 { 392 Var var; 393 T* next; 394 this(T* varZString) 395 { 396 this.next = varZString; 397 popFront(); 398 } 399 @property bool empty() { return var.name is null; } 400 auto front() { return var; } 401 void popFront() 402 { 403 next = skipCharSet!"&"(next); 404 405 if(*next == '\0') { 406 var.name = null; 407 } else { 408 auto namePtr = next; 409 auto valuePtr = findCharPtr(next, '='); 410 411 var.name = namePtr[0..valuePtr - namePtr]; 412 if(*valuePtr == '=') { 413 valuePtr++; 414 next = findCharPtr(valuePtr, '&'); 415 var.value = valuePtr[0..next - valuePtr]; 416 } else { 417 var.value = valuePtr[0..0]; 418 next = valuePtr; 419 } 420 //logf("queryVarsRange name=\"%s\" value=\"%s\"", var.name, var.value); 421 } 422 } 423 } 424 return Range(varZString); 425 } 426 427 // TODO: make this correct 428 bool isValidHtmlToken(char c) 429 { 430 return 431 (c >= 'a' && c <= 'z') || 432 (c >= 'A' && c <= 'Z') || 433 c == '_' || 434 c == '-'; 435 } 436 437 /** 438 Pulls the content-type "token/token" from the "Content-Type: header value. 439 */ 440 inout(char)[] pullContentTypeZString(inout(char)* contentType) 441 { 442 if (contentType == null) 443 return null; 444 445 auto start = contentType; 446 char c; 447 for(;; contentType++) { 448 c = *contentType; 449 if(!isValidHtmlToken(c)) { 450 break; 451 } 452 } 453 if(c == '/') { 454 for(;;) { 455 contentType++; 456 c = *contentType; 457 if(!isValidHtmlToken(c)) { 458 break; 459 } 460 } 461 } 462 return start[0 .. contentType - start]; 463 } 464 465 enum HeaderContentType = "Content-Type: "; 466 467 auto contentTypeArgRange(Char)(Char* contentTypeParams) 468 { 469 struct Pair 470 { 471 Char[] name; 472 Char[] value; 473 } 474 struct Range 475 { 476 Pair current; 477 this(Char* next) 478 { 479 current.value = next[0..0]; 480 popFront(); 481 } 482 bool empty() { return current.name is null; } 483 auto front() { return current; } 484 void popFront() 485 { 486 auto next = (current.value.ptr + current.value.length).skipCharSet!" ;"(); 487 if(*next == '\0') { 488 current.name = null; 489 } else { 490 auto nameLimit = next.findCharPtr('='); 491 current.name = next[0..nameLimit - next]; 492 if(*nameLimit == '\0') { 493 current.value = nameLimit[0..0]; 494 } else { 495 auto valueStart = nameLimit + 1; 496 auto valueEnd = valueStart.findCharPtr(';'); 497 current.value = trimRight2(valueStart[0..valueEnd - valueStart]); 498 } 499 } 500 } 501 } 502 return Range(contentTypeParams); 503 } 504 auto httpHeaderArgRange(Char)(Char[] headerArgs) 505 { 506 struct Pair 507 { 508 Char[] name; 509 Char[] value; 510 } 511 struct Range 512 { 513 Pair current; 514 Char* next; 515 Char* limit; 516 this(Char[] start) 517 { 518 this.next = start.ptr; 519 this.limit = start.ptr + start.length; 520 popFront(); 521 } 522 bool empty() { return current.name is null; } 523 auto front() { return current; } 524 void popFront() 525 { 526 next = next.skipCharSet!" ;"(limit); 527 if(next == limit) { 528 current.name = null; 529 } else { 530 auto nameLimit = next.findCharPtr(limit, '='); 531 current.name = next[0..nameLimit - next]; 532 if(nameLimit == limit) { 533 current.value = nameLimit[0..0]; 534 } else { 535 auto valueStart = nameLimit + 1; 536 next = valueStart.findCharPtr(limit, ';'); 537 current.value = trimRight2(valueStart[0..next - valueStart]).escapeQuotesIfThere; 538 } 539 } 540 } 541 } 542 return Range(headerArgs); 543 } 544 545 inout(char)[] escapeQuotesIfThere(inout(char)[] str) 546 { 547 if (str.length >= 2 && str[0] == '"' && str[$-1] == '"') 548 { 549 return str[1 ..($-1)]; // TODO: escape the quotes properly 550 } 551 return str; 552 } 553 554 inout(char)[] trimNewline(inout(char)[] str) 555 { 556 if(str.length >= 1 && str[$-1] == '\n') 557 { 558 if(str.length >= 2 && str[$-2] == '\r') 559 { 560 return str[0..$-2]; 561 } 562 return str[0..$-1]; 563 } 564 return str; 565 } 566 inout(char)[] trimRight2(inout(char)[] str) 567 { 568 for(; str.length > 0 && str[$-1] == ' '; str.length--) { } 569 return str; 570 } 571 void trimRight(const(char)[]* str) 572 { 573 for(;(*str).length > 0 && (*str)[$-1] == ' '; (*str).length--) { } 574 } 575 576 auto asUpper(T)(T c) 577 { 578 return (c >= 'a' && c <= 'z') ? (c - ('a'-'A')) : c; 579 } 580 bool strEqualNoCase(const(char)[] left, const(char)[] right) 581 { 582 if(left.length != right.length) { 583 return false; 584 } 585 foreach(i; 0..left.length) { 586 if(left[i].asUpper != right[i].asUpper) { 587 return false; 588 } 589 } 590 return true; 591 } 592 593 alias noStrings = tuple!(); 594 template strings(T...) 595 { 596 alias strings = T; 597 } 598 599 __gshared string stdinReader = null; 600 601 template HttpTemplate(Hooks) 602 { 603 static assert(Hooks.CookieVars.length == 0 || hasMember!(Env, "HTTP_COOKIE"), 604 "if you configure any CookieVars, you must include HTTP_COOKIE in your EnvVars"); 605 static assert(Hooks.ReadFormPostData == false || (hasMember!(Env, "CONTENT_TYPE") && hasMember!(Env, "CONTENT_LENGTH")), 606 "if you set ReadFormPostData to true, you must include CONTENT_TYPE and CONTENT_LENGTH in your EnvVars"); 607 enum ParseUrlVars = (Hooks.UrlVars.length > 0 || Hooks.UrlOrFormVars.length > 0); 608 static assert(!ParseUrlVars || hasMember!(Env, "QUERY_STRING"), 609 "if you have any UrlVars or any UrlOrFormVars, you must include QUERY_STRING in your EnvVars"); 610 611 struct HttpTemplate 612 { 613 Env env; 614 Cookies cookies; 615 UrlVars urlVars; 616 FormVars formVars; 617 UrlOrFormVars urlOrFormVars; 618 619 void init() 620 { 621 static if(Hooks.ReadFormPostData) { 622 bool postContentIsUrlFormData = false; 623 } 624 625 // load environment variables 626 { 627 size_t varsLeft = Hooks.EnvVars.length; 628 ENV_LOOP: 629 foreach(envPair; envPairs()) { 630 foreach(varName; Hooks.EnvVars) { 631 if(__traits(getMember, env, varName) is null && envPair.name.strEqualNoCase(varName)) { 632 __traits(getMember, env, varName) = envPair.permanentValue; 633 static if(varName == "HTTP_COOKIE" && Hooks.CookieVars.length > 0) { 634 parseCookies(__traits(getMember, env, varName)); 635 } else static if(varName == "CONTENT_TYPE" && Hooks.ReadFormPostData) { 636 if(__traits(getMember, env, varName).startsWith("application/x-www-form-urlencoded")) { 637 postContentIsUrlFormData = true; 638 } 639 } 640 varsLeft--; 641 if(varsLeft == 0) { 642 break ENV_LOOP; 643 } 644 break; 645 } 646 } 647 } 648 } 649 650 static if(Hooks.ReadFormPostData) { 651 // read post parameters before url params, url takes precedence 652 if(postContentIsUrlFormData) { 653 if(env.CONTENT_LENGTH is null) { 654 throw new Exception("Content-Type header is set but no Content-Length, this is not implemented"); 655 } 656 // TODO: handle errors when this conversion fails 657 auto contentLength = env.CONTENT_LENGTH.to!size_t; 658 auto buffer = new char[contentLength + 1]; 659 { 660 auto readLength = fread(stdin, buffer[0..contentLength]); 661 if(readLength != contentLength) { 662 throw new Exception(format("based on CONTENT_LENGTH, expected %s bytes of data from stdin but only got %s", 663 contentLength, readLength)); 664 } 665 } 666 stdinReader = "the application/x-www-form-urlencoded parser"; 667 buffer[contentLength] = '\0'; 668 size_t formVarsLeft = Hooks.FormVars.length; 669 size_t urlOrFormVarsLeft = Hooks.UrlOrFormVars.length; 670 POST_CONTENT_VAR_LOOP: 671 foreach(var; queryVarsRange(buffer.ptr)) { 672 673 foreach(varName; Hooks.FormVars) { 674 675 if(__traits(getMember, formVars, varName) is null && var.name == varName) { 676 __traits(getMember, formVars, varName) = var.value; 677 static if(hasMember!(Hooks, "DisableVarUriDecoding")) 678 bool doDecode = !Hooks.DisableVarUriDecoding; 679 else 680 bool doDecode = true; 681 if (doDecode) 682 { 683 auto result = tryUriDecode(__traits(getMember, formVars, varName)); 684 if (result is null) 685 { 686 // TODO: handle this error 687 logf("WARNING: failed to decode post form data variable '%s' (TODO: detect and handle this error)", varName); 688 } 689 else 690 { 691 __traits(getMember, formVars, varName) = result; 692 } 693 } 694 695 formVarsLeft--; 696 if(formVarsLeft == 0 && urlOrFormVarsLeft == 0) { 697 break POST_CONTENT_VAR_LOOP; 698 } 699 break; 700 } 701 } 702 foreach(varName; Hooks.UrlOrFormVars) { 703 704 if(__traits(getMember, urlOrFormVars, varName) is null && var.name == varName) { 705 __traits(getMember, urlOrFormVars, varName) = var.value; 706 static if(hasMember!(Hooks, "DisableVarUriDecoding")) 707 bool doDecode = !Hooks.DisableVarUriDecoding; 708 else 709 bool doDecode = true; 710 if (doDecode) 711 { 712 auto result = tryUriDecode(__traits(getMember, urlOrFormVars, varName)); 713 if (result is null) 714 { 715 // TODO: handle this error 716 logf("WARNING: failed to decode post form data variable '%s' (TODO: detect and handle this error)", varName); 717 } 718 else 719 { 720 __traits(getMember, urlOrFormVars, varName) = result; 721 } 722 } 723 urlOrFormVarsLeft--; 724 if(formVarsLeft == 0 && urlOrFormVarsLeft == 0) { 725 break POST_CONTENT_VAR_LOOP; 726 } 727 break; 728 } 729 } 730 } 731 } 732 } 733 734 // read url vars 735 static if(ParseUrlVars) 736 { 737 if(env.QUERY_STRING.ptr) 738 { 739 uriDecode(env.QUERY_STRING.ptr, env.QUERY_STRING.ptr); 740 741 size_t urlVarsLeft = Hooks.UrlVars.length; 742 size_t urlOrFormVarsLeft = Hooks.UrlOrFormVars.length; 743 URL_VAR_LOOP: 744 foreach(var; queryVarsRange(env.QUERY_STRING.ptr)) 745 { 746 foreach(varName; Hooks.UrlVars) 747 { 748 if(__traits(getMember, urlVars, varName) is null && var.name == varName) 749 { 750 __traits(getMember, urlVars, varName) = var.value; 751 urlVarsLeft--; 752 if(urlVarsLeft == 0 && urlOrFormVarsLeft == 0) 753 { 754 break URL_VAR_LOOP; 755 } 756 break; 757 } 758 } 759 foreach(varName; Hooks.UrlOrFormVars) 760 { 761 if(__traits(getMember, urlOrFormVars, varName) is null && var.name == varName) 762 { 763 __traits(getMember, urlOrFormVars, varName) = var.value; 764 urlOrFormVarsLeft--; 765 if(urlVarsLeft == 0 && urlOrFormVarsLeft == 0) 766 { 767 break URL_VAR_LOOP; 768 } 769 break; 770 } 771 } 772 } 773 } 774 } 775 } 776 private void parseCookies(const(char)[] cookieString) 777 { 778 //logf("parseCookies \"%s\"", cookieString); 779 size_t varsLeft = Hooks.CookieVars.length; 780 COOKIE_LOOP: 781 foreach(cookie; cookieRange!(const(char))(cookieString.ptr)) { 782 //logf(" \"%s\" = \"%s\"", cookie.name, cookie.value); 783 foreach(varName; Hooks.CookieVars) { 784 if(__traits(getMember, cookies, varName) is null && cookie.name == varName) { 785 __traits(getMember, cookies, varName) = cookie.value; 786 varsLeft--; 787 if(varsLeft == 0) { 788 break COOKIE_LOOP; 789 } 790 break; 791 } 792 } 793 } 794 } 795 796 static if(hasMember!(Env, "CONTENT_TYPE")/* && !hasMember!(Env, "CONTENT_LENGTH")*/) 797 { 798 799 // returns: an error message on error 800 string getMultipartBoundary(const(char)[]* outBoundary) 801 { 802 auto contentType = pullContentTypeZString(env.CONTENT_TYPE.ptr); 803 if(contentType != "multipart/form-data") 804 return format("Content-Type is not 'multipart/form-data', it is '%s'", contentType); 805 806 foreach(arg; contentTypeArgRange(env.CONTENT_TYPE.ptr + contentType.length)) { 807 if(arg.name == "boundary") { 808 *outBoundary = arg.value; 809 return null; // no error 810 } else { 811 // ignore 812 //assert(0, format("unknown Content-Type param \"%s\"=\"%s\"", arg.name, arg.value)); 813 } 814 } 815 return format("Content-Type '%s' is missing the 'boundary' parameter", env.CONTENT_TYPE); 816 } 817 818 /** 819 Returns: null on success, error message on error 820 */ 821 string uploadFile(const(char)[] fileFormName, char[] uploadBuffer, 822 string delegate(const(char)[] clientFilename, string* serverFilename) onFilename) 823 { 824 // TODO: might need to support variables as well 825 const(char)[] boundary; 826 { 827 auto error = getMultipartBoundary(&boundary); 828 if (error !is null) 829 return error; 830 } 831 auto reader = StdinMultipartReader(uploadBuffer, boundary); 832 833 for (auto partResult = reader.start(); ;partResult = reader.readHeaders()) 834 { 835 if (partResult.isDone) 836 return "missing file from post data"; 837 if (partResult.isError) 838 return partResult.makeErrorMessage(); 839 840 if (partResult.formData.name != "file") 841 return format("got unknown form variable '%s'", partResult.formData.name); 842 843 if (partResult.formData.filename.canFind('/')) 844 return format("filename '%s' contains slashes", partResult.formData.filename); 845 846 string serverFilename = null; 847 { 848 auto error = onFilename(partResult.formData.filename, &serverFilename); 849 if (error !is null) 850 return error; 851 } 852 if (serverFilename is null) 853 serverFilename = partResult.formData.filename; 854 855 auto file = File(serverFilename, "wb"); 856 for (;;) 857 { 858 auto contentResult = reader.readContent(); 859 if (contentResult.isError) 860 return contentResult.makeErrorMessage(); 861 862 //writeln("<pre>got %s bytes of content data</pre>", contentResult.content.length); 863 file.rawWrite(contentResult.content); 864 if (contentResult.gotBoundary) 865 break; 866 } 867 return null; // success 868 } 869 } 870 } 871 872 } 873 874 struct Env 875 { 876 mixin(function() { 877 auto result = ""; 878 foreach(varName; Hooks.EnvVars) { 879 result ~= "char[] " ~ varName ~ "\n;"; 880 } 881 return result; 882 }()); 883 } 884 struct Cookies 885 { 886 mixin(function() { 887 auto result = ""; 888 foreach(varName; Hooks.CookieVars) { 889 result ~= "const(char)[] " ~ varName ~ "\n;"; 890 } 891 return result; 892 }()); 893 } 894 struct UrlVars 895 { 896 mixin(function() { 897 auto result = ""; 898 foreach(varName; Hooks.UrlVars) { 899 result ~= "const(char)[] " ~ varName ~ "\n;"; 900 } 901 return result; 902 }()); 903 } 904 struct FormVars 905 { 906 mixin(function() { 907 auto result = ""; 908 foreach(varName; Hooks.FormVars) { 909 result ~= "const(char)[] " ~ varName ~ "\n;"; 910 } 911 return result; 912 }()); 913 } 914 struct UrlOrFormVars 915 { 916 mixin(function() { 917 auto result = ""; 918 foreach(varName; Hooks.UrlOrFormVars) { 919 result ~= "const(char)[] " ~ varName ~ "\n;"; 920 } 921 return result; 922 }()); 923 } 924 } 925 926 927 // returns: null on success, ErrnoException on error 928 ErrnoException tryOpenFile(File* file, const(char)[] filename, in char[] openmode) 929 { 930 try { 931 *file = File(filename, openmode); 932 return null; 933 } catch(ErrnoException e) { 934 return e; 935 } 936 } 937 938 char[] tryRead(File file) 939 { 940 auto filesize = file.size(); 941 if(filesize + 1 > size_t.max) { 942 assert(0, text(file.name, ": file is too large ", filesize, " > ", size_t.max)); 943 } 944 auto contents = new char[cast(size_t)(filesize + 1)]; // add 1 for '\0' 945 auto readSize = fread(file, contents); 946 assert(filesize == readSize, text("rawRead only read ", readSize, " bytes of ", filesize, " byte file")); 947 contents[cast(size_t)filesize] = '\0'; 948 return contents[0..$-1]; 949 } 950 951 char[] tryReadFile(const(char)[] filename) 952 { 953 File file; 954 try { 955 file = File(filename, "rb"); 956 } catch(ErrnoException e) { 957 return null; 958 } 959 auto filesize = file.size(); 960 if(filesize + 1 > size_t.max) { 961 assert(0, text(filename, ": file is too large ", filesize, " > ", size_t.max)); 962 } 963 auto contents = new char[cast(size_t)(filesize + 1)]; // add 1 for '\0' 964 auto readSize = fread(file, contents); 965 assert(filesize == readSize, text("fread only read ", readSize, " bytes of ", filesize, " byte file")); 966 contents[cast(size_t)filesize] = '\0'; 967 return contents[0..$-1]; 968 } 969 // returns error message 970 string tryWriteFile(T)(const(char)[] filename, const(T)[] content) if(T.sizeof == 1) 971 { 972 File file; 973 try { 974 file = File(filename, "wb"); 975 } catch(ErrnoException e) { 976 return (e.msg is null) ? "failed to open file" : e.msg; // fail 977 } 978 scope(exit) file.close(); 979 file.rawWrite(content); 980 return null; // success 981 } 982 983 @property auto defaultGsharedRandom() 984 { 985 static __gshared Random random; 986 static __gshared bool seeded = false; 987 988 if(!seeded) { 989 random = Random(cast(uint)MonoTime.currTime.ticks); 990 seeded = true; 991 } 992 return &random; 993 } 994 995 void fillRandom(T)(T random, ubyte[] output) 996 { 997 alias UintType = typeof(random.front); 998 999 ubyte shift = UintType.sizeof * 8; 1000 for(size_t outIndex = 0; outIndex < output.length; outIndex++) { 1001 if(shift == 0) { 1002 random.popFront(); 1003 shift = (UintType.sizeof - 1) * 8; 1004 } else { 1005 shift -= 8; 1006 } 1007 output[outIndex] = cast(ubyte)(random.front >> shift); 1008 } 1009 } 1010 void fillRandomHex(T)(T random, char[] hex) 1011 in { assert(hex.length % 2 == 0); } body 1012 { 1013 // TODO: handle the case where hex is large, then 1014 // just generate in chunks 1015 auto binLength = hex.length / 2; 1016 auto binPtr = cast(ubyte*)alloca(binLength); 1017 assert(binPtr !is null); 1018 auto bin = binPtr[0..binLength]; 1019 fillRandom(random, bin); 1020 auto formatLength = sformat(hex, "%s", formatHex(bin)).length; 1021 assert(formatLength == binLength * 2); 1022 } 1023 1024 alias formatJsSingleQuote = formatEscapeSet!(`\`, `\'`); 1025 alias formatJsDoubleQuote = formatEscapeSet!(`\`, `\"`); 1026 alias formatJsonString = formatEscapeSet!(`\`, `\"`); 1027 unittest 1028 { 1029 import more.test; 1030 mixin(scopedTest!"cgi - formatJsonString"); 1031 1032 assert(`` == format("%s", formatJsonString(``))); 1033 assert(`a` == format("%s", formatJsonString(`a`))); 1034 assert(`abcd` == format("%s", formatJsonString(`abcd`))); 1035 1036 assert(`\"` == format("%s", formatJsonString(`"`))); 1037 assert(`\\` == format("%s", formatJsonString(`\`))); 1038 assert(`\"\\` == format("%s", formatJsonString(`"\`))); 1039 assert(`a\"\\` == format("%s", formatJsonString(`a"\`))); 1040 assert(`\"a\\` == format("%s", formatJsonString(`"a\`))); 1041 assert(`\"\\a` == format("%s", formatJsonString(`"\a`))); 1042 assert(`abcd\"\\` == format("%s", formatJsonString(`abcd"\`))); 1043 assert(`\"abcd\\` == format("%s", formatJsonString(`"abcd\`))); 1044 assert(`\"\\abcd` == format("%s", formatJsonString(`"\abcd`))); 1045 } 1046 1047 1048 // returns the amount of characters that were parsed 1049 size_t parseHex(const(char)[] hex, ubyte[] bin) 1050 { 1051 size_t hexIndex = 0; 1052 size_t binIndex = 0; 1053 for(;;) { 1054 if(hexIndex + 1 > hex.length || binIndex >= bin.length) { 1055 break; 1056 } 1057 auto high = hexValue(hex[hexIndex + 0]); 1058 auto low = hexValue(hex[hexIndex + 1]); 1059 if(high == ubyte.max || low == ubyte.max) { 1060 break; 1061 } 1062 bin[binIndex++] = cast(ubyte)(high << 4 | low); 1063 hexIndex += 2; 1064 } 1065 return hexIndex; 1066 } 1067 1068 /** 1069 Returns:`buffer.length` on success, negative on error, otherwise, the number of bytes read before EOF 1070 */ 1071 // 1072 auto readFull(Policy)(char[] buffer) 1073 { 1074 size_t totalRead = 0; 1075 for (;;) 1076 { 1077 if (totalRead == buffer.length) 1078 return totalRead; 1079 auto result = Policy.read(buffer[totalRead .. $]); 1080 if (result <= 0) 1081 { 1082 if (result == 0) 1083 return totalRead; 1084 return result; 1085 } 1086 1087 totalRead += result; 1088 } 1089 } 1090 1091 auto upTo(T)(T array, char toChar) 1092 { 1093 auto charIndex = array.indexOf(toChar); 1094 return (charIndex < 0) ? array : array[0 .. charIndex]; 1095 } 1096 1097 auto indexOfParts(U, T...)(U haystack, T parts) 1098 { 1099 ptrdiff_t offset = 0; 1100 for (;;) 1101 { 1102 auto next = haystack[offset .. $].indexOf(parts[0]); 1103 if (next == -1) 1104 return -1; 1105 offset += next; 1106 next = offset + parts[0].length; 1107 bool mismatch = false; 1108 foreach (part; parts[1 .. $]) 1109 { 1110 if (!haystack[next .. $].startsWith(part)) 1111 { 1112 mismatch = true; 1113 break; 1114 } 1115 next += part.length; 1116 } 1117 if (!mismatch) 1118 return offset; 1119 offset++; 1120 } 1121 } 1122 unittest 1123 { 1124 assert("1234".indexOfParts("2") == 1); 1125 assert("1234".indexOfParts("2", "3") == 1); 1126 assert("1234".indexOfParts("23") == 1); 1127 assert("1234".indexOfParts("23", "4") == 1); 1128 assert("1234".indexOfParts("2", "34") == 1); 1129 assert("123456789".indexOfParts("567", "8") == 4); 1130 assert("123456789".indexOfParts("5", "678") == 4); 1131 assert("123456789".indexOfParts("5", "67", "89") == 4); 1132 assert("1231234123".indexOfParts("1", "2", "34") == 3); 1133 } 1134 1135 struct MultipartFormData 1136 { 1137 string name; 1138 string filename; 1139 string contentType; 1140 void reset() 1141 { 1142 this.name = null; 1143 this.filename = null; 1144 this.contentType = null; 1145 } 1146 } 1147 1148 struct MultipartReaderTemplate(Policy) 1149 { 1150 // TODO: mabye create a mixin for this 1151 static struct ReadHeadersResult 1152 { 1153 static ReadHeadersResult atContent(MultipartFormData formData) { return ReadHeadersResult(formData); } 1154 static ReadHeadersResult done() { return ReadHeadersResult(State.done); } 1155 static ReadHeadersResult policyError(string errorMsg) 1156 { 1157 auto r = ReadHeadersResult(State.policyError); 1158 r.str = errorMsg; 1159 return r; 1160 } 1161 static ReadHeadersResult bufferTooSmallForBoundary() { return ReadHeadersResult(State.bufferTooSmallForBoundary); } 1162 static ReadHeadersResult bufferTooSmallForHeaders() { return ReadHeadersResult(State.bufferTooSmallForHeaders); } 1163 static ReadHeadersResult readError(int errorNumber) 1164 { 1165 auto r = ReadHeadersResult(State.readError); 1166 r.int_ = errorNumber; 1167 return r; 1168 } 1169 static ReadHeadersResult invalidFirstBoundary() { return ReadHeadersResult(State.invalidFirstBoundary); } 1170 static ReadHeadersResult invalidBoundaryPostfix() { return ReadHeadersResult(State.invalidBoundaryPostfix); } 1171 static ReadHeadersResult headerTooBig() { return ReadHeadersResult(State.headerTooBig); } 1172 static ReadHeadersResult eofInsideHeaders() { return ReadHeadersResult(State.eofInsideHeaders); } 1173 static ReadHeadersResult unknownHeader(char[] s) 1174 { 1175 auto r = ReadHeadersResult(State.unknownHeader); 1176 r.charArray = s; 1177 return r; 1178 } 1179 static ReadHeadersResult missingContentDisposition() { return ReadHeadersResult(State.missingContentDisposition); } 1180 static ReadHeadersResult unknownContentDispositionArg(char[] s) 1181 { 1182 auto r = ReadHeadersResult(State.unknownContentDispositionArg); 1183 r.charArray = s; 1184 return r; 1185 } 1186 static ReadHeadersResult contentDispositionMissingFormData() { return ReadHeadersResult(State.contentDispositionMissingFormData); } 1187 static ReadHeadersResult contentDispositionMissingName() { return ReadHeadersResult(State.contentDispositionMissingName); } 1188 1189 private enum State : ubyte 1190 { 1191 atContent, 1192 done, 1193 // error states 1194 errorStates, 1195 policyError = errorStates, 1196 bufferTooSmallForBoundary, 1197 bufferTooSmallForHeaders, 1198 readError, 1199 invalidFirstBoundary, 1200 invalidBoundaryPostfix, 1201 headerTooBig, 1202 eofInsideHeaders, 1203 unknownHeader, 1204 missingContentDisposition, 1205 unknownContentDispositionArg, 1206 contentDispositionMissingFormData, 1207 contentDispositionMissingName, 1208 } 1209 State state; 1210 union 1211 { 1212 MultipartFormData formData; 1213 char[] charArray; 1214 string str; 1215 int int_; 1216 } 1217 private this(State state) 1218 { 1219 this.state = state; 1220 } 1221 private this(MultipartFormData formData) 1222 { 1223 this.state = State.atContent; 1224 this.formData = formData; 1225 } 1226 1227 bool isError() const { return state >= State.errorStates; } 1228 bool isDone() const { return state == State.done; } 1229 1230 string makeErrorMessage() const 1231 { 1232 final switch(state) 1233 { 1234 case State.atContent: return "no error"; 1235 case State.done: return "no error"; 1236 case State.policyError: return str; 1237 case State.bufferTooSmallForBoundary: 1238 return "upload buffer too small (cannot hold boundary)"; 1239 case State.bufferTooSmallForHeaders: 1240 return "upload buffer too small (cannot hold headers)"; 1241 case State.readError: 1242 return format("read failed (e=%d)", int_); 1243 case State.invalidFirstBoundary: 1244 return "invalid upload data (initial boundary is not right)"; 1245 case State.invalidBoundaryPostfix: 1246 return "invalid upload data (invalid boundary postfix)"; 1247 case State.headerTooBig: 1248 return format("invalid upload data (header exceeded max size of %s)", Policy.MaxHeader); 1249 case State.eofInsideHeaders: 1250 return "invalid upload data (input ended inside headers)"; 1251 case State.unknownHeader: 1252 return format("invalid upload data (unrecognized header '%s')", charArray); 1253 case State.missingContentDisposition: 1254 return "invalid upload data (missing Content-Disposition)"; 1255 case State.unknownContentDispositionArg: 1256 return format("invalid upload data (unknown Content-Disposition arg '%s')", charArray); 1257 case State.contentDispositionMissingFormData: 1258 return "invalid upload data (Content-Disposition is missing form-data argument)"; 1259 case State.contentDispositionMissingName: 1260 return "invalid upload data (Content-Disposition is missing name argument)"; 1261 } 1262 } 1263 } 1264 1265 char[] buffer; 1266 const(char)[] boundary; 1267 size_t dataOffset; 1268 size_t dataLimit; 1269 1270 pragma(inline) auto encapsulateBoundaryLength() const 1271 { 1272 return 4 + // "\r\n--" 1273 boundary.length ; // Content-Type boundary string 1274 } 1275 ReadHeadersResult start() 1276 { 1277 { 1278 auto error = Policy.checkForErrorBeforeStart(); 1279 if (error !is null) 1280 return ReadHeadersResult.policyError(error); 1281 } 1282 // TODO: the multipart content could start with a prologue instead 1283 // of starting with the boundary right away 1284 auto firstBoundaryLength = 2 + boundary.length; // "--" ~ boundary 1285 if (buffer.length < firstBoundaryLength) 1286 return ReadHeadersResult.bufferTooSmallForBoundary; 1287 1288 auto result = readFull!Policy(buffer[0 .. firstBoundaryLength]); 1289 if (result != firstBoundaryLength) 1290 { 1291 if (result == 0) 1292 return ReadHeadersResult.done; 1293 return ReadHeadersResult.readError(Policy.getError(result)); 1294 } 1295 if (buffer[0 .. 2] != "--" || 1296 buffer[2 .. 2 + boundary.length] != boundary) 1297 return ReadHeadersResult.invalidFirstBoundary; 1298 dataOffset = 0; 1299 dataLimit = 0; 1300 return readHeaders(); 1301 } 1302 1303 /** shift current data to the beginning of the buffer */ 1304 private void shiftData() 1305 { 1306 auto saveLength = dataLimit - dataOffset; 1307 if (saveLength > 0) 1308 { 1309 memmove(buffer.ptr, buffer.ptr + dataOffset, saveLength); 1310 } 1311 dataOffset = 0; 1312 dataLimit = saveLength; 1313 } 1314 1315 // returns: positive on success, 0 on EOF, negative on error 1316 private auto ensureSmallDataSizeAvailable(uint size) 1317 in { /*assert(size <= buffer.length);*/ } do 1318 { 1319 auto dataSize = dataLimit - dataOffset; 1320 if (size > dataSize) 1321 { 1322 shiftData(); 1323 auto bufferAvailable = buffer.length - dataLimit; 1324 auto sizeToRead = size - dataSize; 1325 // NOTE: sizeToRead must be <= (buffer.length - dataLimit) 1326 // because size <= buffer.length 1327 auto result = Policy.read(buffer[dataLimit .. dataLimit + sizeToRead]); 1328 if (result <= 0) 1329 return result; 1330 dataLimit += result; 1331 if (result != sizeToRead) 1332 return -1; 1333 return result; 1334 } 1335 return 1; // success 1336 } 1337 1338 ReadHeadersResult readHeaders() 1339 { 1340 // check whether we start with a "\r\n" or "--" 1341 { 1342 auto result = ensureSmallDataSizeAvailable(2); 1343 if (result <= 0) 1344 { 1345 if (result == 0) 1346 return ReadHeadersResult.eofInsideHeaders; 1347 return ReadHeadersResult.readError(Policy.getError(result)); 1348 } 1349 } 1350 auto boundaryPostfix = buffer[dataOffset .. dataOffset + 2]; 1351 if (boundaryPostfix == "\r\n") 1352 dataOffset += 2; 1353 else if (boundaryPostfix == "--") 1354 return ReadHeadersResult.done; 1355 else 1356 return ReadHeadersResult.invalidBoundaryPostfix; 1357 1358 MultipartFormData formData; 1359 auto checkIndex = dataOffset; 1360 //import std.stdio; writefln("+ readHeaders (checkIndex=%s, dataSize=%s)", checkIndex, dataLimit - dataOffset); 1361 for (;;) 1362 { 1363 // find the end of the next header 1364 auto newlinePos = buffer[checkIndex .. dataLimit].indexOf("\n"); 1365 //import std.stdio; writefln("newlinePos=%s, (checkIndex=%s)", newlinePos, checkIndex); 1366 if (newlinePos == -1) 1367 { 1368 shiftData(); 1369 if (dataLimit > Policy.MaxHeader) 1370 return ReadHeadersResult.headerTooBig(); 1371 auto bufferAvailable = buffer.length - dataLimit; 1372 if (bufferAvailable == 0) 1373 return ReadHeadersResult.bufferTooSmallForHeaders; 1374 // TODO: don't read TOO much, we don't want to 1375 // end up "shifting" too much data 1376 auto result = Policy.read(buffer[dataLimit .. $]); 1377 //import std.stdio; writefln("read %s bytes", result); 1378 if (result <= 0) 1379 { 1380 if (result == 0) 1381 { 1382 if (dataOffset == dataLimit) { 1383 return ReadHeadersResult.done; 1384 } 1385 return ReadHeadersResult.eofInsideHeaders; 1386 } 1387 return ReadHeadersResult.readError(Policy.getError(result)); 1388 } 1389 checkIndex = dataLimit; 1390 dataLimit += result; 1391 } 1392 else 1393 { 1394 auto newlineOffset = checkIndex + newlinePos; 1395 auto header = buffer[dataOffset .. newlineOffset - 1]; 1396 dataOffset = newlineOffset + 1; 1397 //import std.stdio; writefln("header = '%s'", header.asciiFormatEscaped); 1398 1399 if (header.length == 0) 1400 { 1401 if (formData.name is null) 1402 return ReadHeadersResult.missingContentDisposition; 1403 return ReadHeadersResult.atContent(formData); 1404 } 1405 import std.algorithm : skipOver; 1406 if (header.skipOver("Content-Disposition: ")) 1407 { 1408 if (!header.skipOver("form-data")) 1409 return ReadHeadersResult.contentDispositionMissingFormData; 1410 foreach(arg; httpHeaderArgRange(header)) 1411 { 1412 if(arg.name == "name") 1413 formData.name = Policy.allocName(arg.value); 1414 else if(arg.name == "filename") 1415 formData.filename = Policy.allocName(arg.value); 1416 else 1417 return ReadHeadersResult.unknownContentDispositionArg(arg.name); 1418 } 1419 if (formData.name is null) 1420 return ReadHeadersResult.contentDispositionMissingName; 1421 //import std.stdio; writefln("formData.name = '%s'", formData.name); 1422 } 1423 else if (header.skipOver("Content-Type: ")) 1424 { 1425 formData.contentType = Policy.allocName(header); 1426 } 1427 else 1428 { 1429 return ReadHeadersResult.unknownHeader(header.upTo(':')); 1430 } 1431 checkIndex = dataOffset; 1432 } 1433 } 1434 } 1435 1436 static struct ReadContentResult 1437 { 1438 static ReadContentResult contentButNoBoundaryYet(char[] content) 1439 { 1440 ReadContentResult result = void; 1441 result.state = State.contentButNoBoundaryYet; 1442 result._content = content; 1443 return result; 1444 } 1445 static ReadContentResult contentWithBoundary(char[] content) 1446 { 1447 ReadContentResult result = void; 1448 result.state = State.contentWithBoundary; 1449 result._content = content; 1450 return result; 1451 } 1452 static ReadContentResult bufferTooSmallForBoundary() 1453 { return ReadContentResult(State.bufferTooSmallForBoundary); } 1454 static ReadContentResult noEndingBoundary() 1455 { return ReadContentResult(State.noEndingBoundary); } 1456 static ReadContentResult readError(int errorNumber) 1457 { 1458 auto r = ReadContentResult(State.readError); 1459 r.int_ = errorNumber; 1460 return r; 1461 } 1462 1463 private enum State : ubyte 1464 { 1465 // gotContent states 1466 contentButNoBoundaryYet, 1467 contentWithBoundary, 1468 // error states 1469 errorStates, 1470 bufferTooSmallForBoundary = errorStates, 1471 noEndingBoundary, 1472 readError, 1473 } 1474 State state; 1475 union 1476 { 1477 private char[] _content; 1478 private int int_; 1479 } 1480 bool isError() const { return state >= State.errorStates; } 1481 char[] content() const { return cast(char[])_content; } 1482 bool gotBoundary() const { return state == State.contentWithBoundary; } 1483 1484 string makeErrorMessage() const 1485 { 1486 final switch(state) 1487 { 1488 case State.contentButNoBoundaryYet: return "no error"; 1489 case State.contentWithBoundary: return "no error"; 1490 case State.bufferTooSmallForBoundary: 1491 return "upload buffer too small (cannot hold boundary)"; 1492 case State.noEndingBoundary: 1493 return "invalid upload data (missing terminating boundary)"; 1494 case State.readError: 1495 return format("read failed (e=%d)", int_); 1496 } 1497 } 1498 } 1499 ReadContentResult readContent() 1500 { 1501 auto checkIndex = dataOffset; 1502 //import std.stdio; writefln("+ readContent (checkIndex=%s, dataSize=%s)", checkIndex, dataLimit - dataOffset); 1503 for (;;) 1504 { 1505 //import std.stdio; writefln("readContent Loop"); 1506 auto nextBoundaryPos = buffer[checkIndex .. dataLimit].indexOfParts("\r\n--", boundary); 1507 if (nextBoundaryPos == -1) 1508 { 1509 //import std.stdio; writefln("boundary not found!"); 1510 auto dataLength = dataLimit - dataOffset; 1511 if (dataLength >= encapsulateBoundaryLength) { 1512 auto returnLimit = dataLimit - (encapsulateBoundaryLength - 1); 1513 auto content = buffer[dataOffset .. returnLimit]; 1514 //import std.stdio; writefln("returning data %s to %s", dataOffset, returnLimit); 1515 dataOffset = returnLimit; 1516 return ReadContentResult.contentButNoBoundaryYet(content); 1517 } 1518 // we need to read more data 1519 shiftData(); 1520 auto bufferAvailable = buffer.length - dataLimit; 1521 //import std.stdio; writefln("readContent shift (data=%s, available=%s)", dataLimit, bufferAvailable); 1522 if (bufferAvailable == 0) 1523 return ReadContentResult.bufferTooSmallForBoundary; 1524 // TODO: don't read TOO much, we don't want to 1525 // end up "shifting" too much data 1526 auto result = Policy.read(buffer[dataLimit .. $]); 1527 //import std.stdio; writefln("read %s bytes (in readContent)", result); 1528 if (result <= 0) 1529 { 1530 if (result == 0) 1531 return ReadContentResult.noEndingBoundary; 1532 return ReadContentResult.readError(Policy.getError(result)); 1533 } 1534 if (dataLimit > (encapsulateBoundaryLength - 1)) 1535 checkIndex = dataLimit - (encapsulateBoundaryLength - 1); 1536 else 1537 checkIndex = 0; 1538 dataLimit += result; 1539 // import std.stdio; writefln("checkIndex = %s, dataLimit=%s, encapsulateBoundaryLength=%s check='%s'", 1540 // checkIndex, dataLimit, encapsulateBoundaryLength, buffer[checkIndex .. dataLimit]); 1541 // import std.stdio; writefln("checkIndex = %s, dataLimit=%s, encapsulateBoundaryLength=%s", 1542 // checkIndex, dataLimit, encapsulateBoundaryLength); 1543 } 1544 else 1545 { 1546 //import std.stdio; writefln("foundBoundary!"); 1547 auto nextBoundaryIndex = checkIndex + nextBoundaryPos; 1548 auto contentStart = dataOffset; 1549 dataOffset = nextBoundaryIndex + encapsulateBoundaryLength; 1550 return ReadContentResult.contentWithBoundary(buffer[contentStart .. nextBoundaryIndex]); 1551 } 1552 } 1553 } 1554 } 1555 1556 version (linux) 1557 { 1558 extern(C) ptrdiff_t read(int fd, void* ptr, size_t len); 1559 } 1560 else static assert(0, "read function not implemented on this platform"); 1561 1562 private struct StdinMultipartReaderPolicy 1563 { 1564 enum MaxHeader = 200; 1565 // returns error message if we cannot start reading 1566 static string checkForErrorBeforeStart() 1567 { 1568 if (stdinReader) 1569 return format("CodeBug: cannot use StdinMultipartReader because stdin was already read by '%s'", stdinReader); 1570 stdinReader = "multipartReader"; 1571 return null; // no errors 1572 } 1573 static auto read(char[] buffer) 1574 { 1575 return .read(0, buffer.ptr, buffer.length); 1576 } 1577 static auto getError(ptrdiff_t result) 1578 { 1579 static import core.stdc.errno; 1580 return core.stdc.errno.errno; 1581 } 1582 static auto allocName(const(char)[] name) 1583 { 1584 return name.idup; 1585 } 1586 } 1587 alias StdinMultipartReader = MultipartReaderTemplate!StdinMultipartReaderPolicy; 1588 1589 unittest 1590 { 1591 static char[] inBuffer; 1592 static size_t inOffset; 1593 static size_t inLimit; 1594 inBuffer = new char[3000]; 1595 static void setInData(string s) 1596 { 1597 //import std.stdio; writefln("inData '%s'", s); 1598 inBuffer[0 .. s.length] = s; 1599 inOffset = 0; 1600 inLimit = s.length; 1601 } 1602 static string generateTestContent(size_t size) 1603 { 1604 static hexmap = "0123456789abcdef"; 1605 auto content = new char[size]; 1606 foreach (i; 0 .. size) { 1607 content[i] = hexmap[i & 0b1111]; 1608 } 1609 return cast(string)content; 1610 } 1611 static struct TestMultipartReaderPolicy 1612 { 1613 enum MaxHeader = 200; 1614 pragma(inline) 1615 static string checkForErrorBeforeStart() 1616 { 1617 return null; // no error 1618 } 1619 pragma(inline) 1620 static auto read(char[] buffer) 1621 { 1622 //import std.stdio; writefln("ReadCall (in %s-%s) (size=%s)", inOffset, inLimit, buffer.length); 1623 auto readSize = inLimit - inOffset; 1624 if (buffer.length < readSize) 1625 readSize = buffer.length; 1626 buffer[0 .. readSize] = inBuffer[inOffset .. inOffset + readSize]; 1627 inOffset += readSize; 1628 return readSize; 1629 } 1630 static auto getError(ptrdiff_t result) 1631 { 1632 assert(0, "not implemented"); 1633 return result; 1634 } 1635 static auto allocName(const(char)[] name) 1636 { 1637 return name.idup; 1638 } 1639 } 1640 alias TestMultipartReader = MultipartReaderTemplate!TestMultipartReaderPolicy; 1641 char[ 1] buffer1; 1642 char[ 4] buffer4; 1643 char[100] buffer100; 1644 { 1645 setInData(""); 1646 auto reader = TestMultipartReader(buffer100, "MYBOUNDARY"); 1647 assert(reader.start().isDone); 1648 } 1649 { 1650 setInData("--MYBOUNDARY\r\n\r\n"); 1651 auto reader = TestMultipartReader(buffer100, "MYBOUNDARY"); 1652 assert(reader.start().state == TestMultipartReader.ReadHeadersResult.State.missingContentDisposition); 1653 } 1654 { 1655 setInData("--MYBOUNDARY\r\n" 1656 ~ "Content-Disposition: form-data; name=\"TestVariable\"\r\n\r\n"); 1657 auto reader = TestMultipartReader(buffer100, "MYBOUNDARY"); 1658 auto result = reader.start(); 1659 assert(!result.isError); 1660 } 1661 { 1662 setInData("--MYBOUNDARY\r\n" 1663 ~ "Content-Disposition: form-data; name=\"TestVar\"\r\n" 1664 ~ "Content-Type: plain/text\r\n\r\n"); 1665 auto reader = TestMultipartReader(buffer100, "MYBOUNDARY"); 1666 auto result = reader.start(); 1667 assert(!result.isError); 1668 1669 } 1670 { 1671 setInData("--MYBOUNDARY\r\n" 1672 ~ "Content-Disposition: form-data; name=\"TestVar\"\r\n" 1673 ~ "Content-Type: plain/text\r\n\r\n" 1674 ~ "\r\n--MYBOUNDARY\r\n"); 1675 auto reader = TestMultipartReader(buffer100, "MYBOUNDARY"); 1676 { 1677 auto result = reader.start(); 1678 assert(!result.isError); 1679 } 1680 { 1681 auto result = reader.readContent(); 1682 assert(result.gotBoundary); 1683 assert(result.content.length == 0); 1684 } 1685 { 1686 auto result = reader.readHeaders(); 1687 assert(result.isDone); 1688 } 1689 } 1690 1691 static void readAndDropContent(TestMultipartReader* reader) 1692 { 1693 for (;;) 1694 { 1695 auto result = reader.readContent(); 1696 assert(!result.isError); 1697 if (result.gotBoundary) 1698 return; 1699 } 1700 } 1701 static void readAndDropContentSize(TestMultipartReader* reader, size_t contentLeft) 1702 { 1703 for (;;) 1704 { 1705 auto result = reader.readContent(); 1706 assert(!result.isError); 1707 if (result.content.length > contentLeft) 1708 { 1709 import std.stdio; 1710 writefln("result.content.length %s contentLeft %s", result.content.length, contentLeft); 1711 } 1712 assert(result.content.length <= contentLeft); 1713 contentLeft -= result.content.length; 1714 if (contentLeft == 0) 1715 { 1716 if (result.gotBoundary) 1717 return; 1718 } 1719 assert(!result.gotBoundary); 1720 } 1721 } 1722 1723 foreach (bufferSize; 50 .. 101) 1724 { 1725 foreach (contentSize; 0 .. 301) 1726 { 1727 setInData("--MYBOUNDARY\r\n" 1728 ~ "Content-Disposition: form-data; name=\"TestVar\"\r\n" 1729 ~ "Content-Type: plain/text\r\n\r\n" 1730 ~ generateTestContent(contentSize) 1731 ~ "\r\n--MYBOUNDARY\r\n"); 1732 auto reader = TestMultipartReader(buffer100[0 .. bufferSize], "MYBOUNDARY"); 1733 { 1734 auto result = reader.start(); 1735 assert(!result.isError); 1736 } 1737 readAndDropContentSize(&reader, contentSize); 1738 { 1739 auto result = reader.readHeaders(); 1740 //import std.stdio; writefln("state = %s", result.state); 1741 assert(result.isDone); 1742 } 1743 } 1744 } 1745 1746 foreach (bufferSize; 60 .. 101) 1747 { 1748 setInData( "------WebKitFormBoundaryDL9nQxvP3BPx3UvL\r\n" 1749 ~ "Content-Disposition: form-data; name=\"TestVariable1\"\r\n" 1750 ~ "\r\n" 1751 ~ "TestValue1\r\n" 1752 ~ "------WebKitFormBoundaryDL9nQxvP3BPx3UvL\r\n" 1753 ~ "Content-Disposition: form-data; name=\"TestVariable2\"\r\n" 1754 ~ "\r\n" 1755 ~ "TestValue2\r\n" 1756 ~ "------WebKitFormBoundaryDL9nQxvP3BPx3UvL\r\n" 1757 ~ "Content-Disposition: form-data; name=\"TestVariable3\"\r\n" 1758 ~ "\r\n" 1759 ~ "TestValue3\r\n" 1760 ~ "------WebKitFormBoundaryDL9nQxvP3BPx3UvL\r\n" 1761 ~ "Content-Disposition: form-data; name=\"file\"; filename=\"10bytesandnewline.txt\"\r\n" 1762 ~ "Content-Type: text/plain\r\n" 1763 ~ "\r\n" 1764 ~ "0123456789\r\n" 1765 ~ "\r\n" 1766 ~ "------WebKitFormBoundaryDL9nQxvP3BPx3UvL--\r\n"); 1767 auto reader = TestMultipartReader(buffer100[0 .. bufferSize], "----WebKitFormBoundaryDL9nQxvP3BPx3UvL"); 1768 { 1769 auto result = reader.start(); 1770 assert(!result.isError); 1771 } 1772 readAndDropContentSize(&reader, 10); 1773 } 1774 1775 } 1776 1777 1778 // Assumption: str is null terminated 1779 alias zStringByLine = delimited!'\n'.sentinalRange!('\0', const(char)); 1780 alias stringByLine = delimited!'\n'.range; 1781 unittest 1782 { 1783 import more.test; 1784 mixin(scopedTest!"cgi - strings by line"); 1785 1786 auto expected = ["a", "bcd", "ef"]; 1787 { 1788 size_t i = 0; 1789 foreach(line; zStringByLine("a\nbcd\nef\0")) { 1790 assert(line == expected[i]); 1791 i++; 1792 } 1793 } 1794 { 1795 size_t i = 0; 1796 foreach(line; stringByLine("a\nbcd\nef")) { 1797 assert(line == expected[i]); 1798 i++; 1799 } 1800 } 1801 } 1802 1803 // 1804 // Functions to operate on delimited data 1805 // 1806 // delimited data is of the form 1807 // <data> [<delimiter> <data>] * 1808 // 1809 template delimited(char delimiter) 1810 { 1811 auto sentinalRange(char sentinal = '\0', T)(T* str) 1812 { 1813 struct Range 1814 { 1815 T[] current; 1816 T* next; 1817 this(T* str) 1818 { 1819 this.next = str; 1820 popFront(); 1821 } 1822 bool empty() { return current.ptr == null; } 1823 auto front() { return current; } 1824 void popFront() 1825 { 1826 auto start = next; 1827 if(*start == sentinal) { 1828 current = null; 1829 return; 1830 } 1831 1832 for(;;next++) { 1833 auto c = *next; 1834 if(c == delimiter) { 1835 current = start[0..next-start]; 1836 next++; 1837 return; 1838 } 1839 if(c == '\0') { 1840 current = start[0..next-start]; 1841 return; 1842 } 1843 } 1844 } 1845 } 1846 return Range(str); 1847 } 1848 auto range(T)(T[] str) 1849 { 1850 struct Range 1851 { 1852 T[] current; 1853 T[] rest; 1854 this(T[] str) 1855 { 1856 rest = str; 1857 popFront(); 1858 } 1859 bool empty() { return current.ptr == null; } 1860 auto front() { return current; } 1861 void popFront() 1862 { 1863 if(rest.length == 0) { 1864 current = null; 1865 return; 1866 } 1867 1868 for(size_t i = 0;;) { 1869 auto c = rest[i]; 1870 if(c == delimiter) { 1871 current = rest[0..i]; 1872 rest = rest[i + 1..$]; 1873 return; 1874 } 1875 i++; 1876 if(i >= rest.length) { 1877 current = rest; 1878 rest = rest[$..$]; 1879 return; 1880 } 1881 } 1882 } 1883 } 1884 return Range(str); 1885 } 1886 1887 enum findFormatCode = q{ 1888 { 1889 if (haystack == null) 1890 return null; 1891 for(;;) { 1892 auto nextLimit = %s; 1893 auto value = haystack[0..nextLimit - haystack]; 1894 if(value == needle) { 1895 return value; 1896 } 1897 if(%s) { 1898 return null; 1899 } 1900 haystack = nextLimit + 1; 1901 } 1902 } 1903 }; 1904 1905 mixin(q{ 1906 inout(char)[] find(char sentinal = '\0')(inout(char)* haystack, const(char)[] needle) 1907 } ~ format(findFormatCode, q{haystack.findCharPtr!sentinal(delimiter)}, q{*nextLimit == sentinal})); 1908 mixin(q{ 1909 inout(char)[] find(inout(char)* haystack, const(char)* limit, const(char)[] needle) 1910 } ~ format(findFormatCode, q{haystack.findCharPtr(limit, delimiter)}, q{nextLimit == limit})); 1911 pragma(inline) 1912 inout(char)[] find(inout(char)[] haystack, const(char)[] needle) 1913 { 1914 return find(haystack.ptr, haystack.ptr + haystack.length, needle); 1915 } 1916 1917 // Appends a string onto an existing string with a delimiter. It checks whether 1918 // or not the previous string already has a delimiter and won't include the delimiter 1919 // in that case 1920 inout(char)[] append(const(char)[] current, inout(char)[] new_) 1921 { 1922 if(current.length == 0) { 1923 return new_; 1924 } 1925 if(current[$-1] == delimiter) { 1926 return cast(inout(char)[])current ~ new_; 1927 } 1928 1929 return cast(inout(char)[])current ~ delimiter ~ new_; 1930 } 1931 auto removeItem(inout(char)[] current, const(char)[] item) 1932 { 1933 return removeItem(current, item.ptr - current.ptr, item.ptr + item.length - current.ptr); 1934 } 1935 auto removeItemWithLength(inout(char)[] current, size_t itemStart, size_t itemLength) 1936 { 1937 return removeItem(current, itemStart, itemStart + itemLength); 1938 } 1939 // doesn't allocate memory if the item is at the beginning or end 1940 auto removeItem(inout(char)[] current, size_t itemStart, size_t itemLimit) in { 1941 assert(itemStart <= itemLimit); 1942 assert(itemLimit <= current.length); 1943 assert(itemStart == 0 || current[itemStart - 1] == delimiter); 1944 assert(itemLimit == current.length || current[itemLimit] == delimiter); } body 1945 { 1946 if(itemStart == 0) { 1947 if(itemLimit == current.length) { 1948 return current[0..0]; 1949 } 1950 assert(current[itemLimit] == delimiter); 1951 return current[itemLimit + 1..$]; 1952 } 1953 assert(current[itemStart - 1] == delimiter); 1954 if(itemLimit == current.length) { 1955 return current[0..itemStart - 1]; 1956 } 1957 return current[0..itemStart - 1] ~ current[itemLimit .. $]; 1958 } 1959 } 1960 unittest 1961 { 1962 import more.test; 1963 mixin(scopedTest!"cgi - delimited 1"); 1964 1965 assert("" == delimited!':'.find(cast(char*)null, "")); 1966 assert("" == delimited!':'.find("\0".ptr, "")); 1967 1968 assert(null == delimited!':'.find("\0".ptr, "a")); 1969 assert("a" == delimited!':'.find("a\0".ptr, "a")); 1970 assert(null == delimited!':'.find("ab\0".ptr, "a")); 1971 1972 assert(null == delimited!':'.find("\0".ptr, "abcd")); 1973 assert(null == delimited!':'.find("abc\0".ptr, "abcd")); 1974 assert("abcd" == delimited!':'.find("abcd\0".ptr, "abcd")); 1975 assert(null == delimited!':'.find("abcde\0".ptr, "abcd")); 1976 1977 assert(null == delimited!':'.find("b:c\0".ptr, "a")); 1978 assert("a" == delimited!':'.find("b:a\0".ptr, "a")); 1979 assert(null == delimited!':'.find("b:ab\0".ptr, "a")); 1980 assert("a" == delimited!':'.find("a:b\0".ptr, "a")); 1981 assert(null == delimited!':'.find("ab:b\0".ptr, "a")); 1982 assert(null == delimited!':'.find("d:b:e\0".ptr, "a")); 1983 assert("a" == delimited!':'.find("d:b:e:a\0".ptr, "a")); 1984 assert(null == delimited!':'.find("d:b:e:ab\0".ptr, "a")); 1985 1986 assert(null == delimited!':'.find("bbbb:cccc\0".ptr, "aaaa")); 1987 assert("aaaa" == delimited!':'.find("aaaa:bbbb\0".ptr, "aaaa")); 1988 assert("aaaa" == delimited!':'.find("bbbb:aaaa\0".ptr, "aaaa")); 1989 assert(null == delimited!':'.find("bbbb:aaaae\0".ptr, "aaaa")); 1990 assert(null == delimited!':'.find("dddd:bbbb:eeee\0".ptr, "aaaa")); 1991 assert("aaaa" == delimited!':'.find("aaaa:bbbb:eeee\0".ptr, "aaaa")); 1992 assert("aaaa" == delimited!':'.find("bbbb:aaaa:eeee\0".ptr, "aaaa")); 1993 assert("aaaa" == delimited!':'.find("bbbb:eeee:aaaa\0".ptr, "aaaa")); 1994 assert("aaaa" == delimited!':'.find("dddd:bbbb:eeee:aaaa\0".ptr, "aaaa")); 1995 assert(null == delimited!':'.find("ddddd:bbbb:e:aaaaa\0".ptr, "aaaa")); 1996 1997 // test the other find variations 1998 assert("a" == delimited!':'.find("a", "a")); 1999 } 2000 2001 unittest 2002 { 2003 import more.test; 2004 mixin(scopedTest!"cgi - delimited 2"); 2005 2006 assert("" == delimited!':'.removeItem(null, 0, 0)); 2007 assert("" == delimited!':'.removeItem("", 0, 0)); 2008 assert("" == delimited!':'.removeItem("a", 0, 1)); 2009 assert("b" == delimited!':'.removeItem("a:b", 0, 1)); 2010 assert("a" == delimited!':'.removeItem("a:b", 2, 3)); 2011 2012 assert("b:c" == delimited!':'.removeItem("a:b:c", 0, 1)); 2013 assert("a:c" == delimited!':'.removeItem("a:b:c", 2, 3)); 2014 assert("a:b" == delimited!':'.removeItem("a:b:c", 4, 5)); 2015 2016 assert("efgh:ijkl" == delimited!':'.removeItem("abcd:efgh:ijkl", 0, 4)); 2017 assert("abcd:ijkl" == delimited!':'.removeItem("abcd:efgh:ijkl", 5, 9)); 2018 assert("abcd:efgh" == delimited!':'.removeItem("abcd:efgh:ijkl", 10, 14)); 2019 }