1 /** 2 * A simple HTTP server. 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 * Stéphan Kochen <stephan@kochen.nl> 12 * Vladimir Panteleev <ae@cy.md> 13 * Simon Arlott 14 */ 15 16 module ae.net.http.server; 17 18 import std.algorithm.mutation : move; 19 import std.conv; 20 import std.datetime; 21 import std.exception; 22 import std.range; 23 import std.socket; 24 import std.string; 25 import std.uri; 26 27 import ae.net.asockets; 28 import ae.net.ietf.headerparse; 29 import ae.net.ietf.headers; 30 import ae.net.ssl; 31 import ae.sys.data; 32 import ae.sys.dataset : bytes, shift, DataVec, joinToGC; 33 import ae.sys.log; 34 import ae.utils.array; 35 import ae.utils.container.listnode; 36 import ae.utils.exception; 37 import ae.utils.text; 38 import ae.utils.textout; 39 40 public import ae.net.http.common; 41 42 debug(HTTP) import std.stdio : stderr; 43 44 // TODO: 45 // - Decouple protocol from network operations. 46 // This should work on IConnection everywhere. 47 // - Unify HTTP client and server connections. 48 // Aside the first line, these are pretty much the same protocols. 49 // - We have more than one axis of parameters: 50 // transport socket type, whether TLS is enabled, possibly more. 51 // We have only one axis of polymorphism (class inheritance), 52 // so combinations such as UNIX TLS HTTP server are difficult to represent. 53 // Refactor to fix this. 54 // - HTTP bodies have stream semantics, and should be represented as such. 55 56 /// The base class for an incoming connection to a HTTP server, 57 /// unassuming of transport. 58 class BaseHttpServerConnection 59 { 60 public: 61 TimeoutAdapter timer; /// Time-out adapter. 62 IConnection conn; /// Connection used for this HTTP connection. 63 64 HttpRequest currentRequest; /// The current in-flight request. 65 bool persistent; /// Whether we will keep the connection open after the request is handled. 66 bool optimizeResponses = true; /// Whether we should compress responses according to the request headers. 67 bool satisfyRangeRequests = true; /// Whether we should follow "Range" request headers. 68 69 bool connected = true; /// Are we connected now? 70 Logger log; /// Optional HTTP log. 71 72 void delegate(HttpRequest request) handleRequest; /// Callback to handle a fully received request. 73 74 protected: 75 string protocol; 76 DataVec inBuffer; 77 sizediff_t expect; 78 size_t responseSize; 79 bool requestProcessing; // user code is asynchronously processing current request 80 bool firstRequest = true; 81 Duration timeout = HttpServer.defaultTimeout; 82 bool timeoutActive; 83 string banner; 84 85 this(IConnection c) 86 { 87 debug (HTTP) debugLog("New connection from %s", remoteAddressStr(null)); 88 89 if (timeout != Duration.zero) 90 { 91 timer = new TimeoutAdapter(c); 92 timer.setIdleTimeout(timeout); 93 c = timer; 94 } 95 96 this.conn = c; 97 conn.handleReadData = &onNewRequest; 98 conn.handleDisconnect = &onDisconnect; 99 100 timeoutActive = true; 101 } 102 103 debug (HTTP) 104 final void debugLog(Args...)(Args args) 105 { 106 stderr.writef("[%s %s] ", Clock.currTime(), cast(void*)this); 107 stderr.writefln(args); 108 } 109 110 final void onNewRequest(Data data) 111 { 112 try 113 { 114 inBuffer ~= data; 115 debug (HTTP) debugLog("Receiving start of request (%d new bytes, %d total)", data.length, inBuffer.bytes.length); 116 117 string reqLine; 118 Headers headers; 119 120 if (!parseHeaders(inBuffer, reqLine, headers)) 121 { 122 debug (HTTP) debugLog("Headers not yet received. Data in buffer:\n%s---", inBuffer.joinToGC().as!string); 123 return; 124 } 125 126 debug (HTTP) 127 { 128 debugLog("Headers received:"); 129 debugLog("> %s", reqLine); 130 foreach (name, value; headers) 131 debugLog("> %s: %s", name, value); 132 } 133 134 currentRequest = new HttpRequest; 135 currentRequest.protocol = protocol; 136 currentRequest.parseRequestLine(reqLine); 137 currentRequest.headers = headers; 138 139 auto connection = toLower(currentRequest.headers.get("Connection", null)); 140 switch (currentRequest.protocolVersion) 141 { 142 case "1.0": 143 persistent = connection == "keep-alive"; 144 break; 145 default: // 1.1+ 146 persistent = connection != "close"; 147 break; 148 } 149 debug (HTTP) debugLog("This %s connection %s persistent", currentRequest.protocolVersion, persistent ? "IS" : "is NOT"); 150 151 expect = 0; 152 if ("Content-Length" in currentRequest.headers) 153 expect = to!size_t(currentRequest.headers["Content-Length"]); 154 155 if (expect > 0) 156 { 157 if (expect > inBuffer.bytes.length) 158 conn.handleReadData = &onContinuation; 159 else 160 processRequest(inBuffer.shift(expect)); 161 } 162 else 163 processRequest(DataVec.init); 164 } 165 catch (CaughtException e) 166 { 167 debug (HTTP) debugLog("Exception onNewRequest: %s", e); 168 HttpResponse response; 169 debug 170 { 171 response = new HttpResponse(); 172 response.status = HttpStatusCode.InternalServerError; 173 response.statusMessage = HttpResponse.getStatusMessage(HttpStatusCode.InternalServerError); 174 response.headers["Content-Type"] = "text/plain"; 175 response.data = DataVec(Data(e.toString().asBytes)); 176 } 177 sendResponse(response); 178 } 179 } 180 181 void onDisconnect(string reason, DisconnectType type) 182 { 183 debug (HTTP) debugLog("Disconnect: %s", reason); 184 connected = false; 185 } 186 187 final void onContinuation(Data data) 188 { 189 debug (HTTP) debugLog("Receiving continuation of request: \n%s---", cast(string)data.unsafeContents); 190 inBuffer ~= data; 191 192 if (!requestProcessing && inBuffer.bytes.length >= expect) 193 { 194 debug (HTTP) debugLog("%s/%s", inBuffer.bytes.length, expect); 195 processRequest(inBuffer.shift(expect)); 196 } 197 } 198 199 final void processRequest(DataVec data) 200 { 201 debug (HTTP) debugLog("processRequest (%d bytes)", data.bytes.length); 202 currentRequest.data = move(data); 203 timeoutActive = false; 204 if (timer) 205 timer.cancelIdleTimeout(); 206 if (handleRequest) 207 { 208 // Log unhandled exceptions, but don't mess up the stack trace 209 //scope(failure) logRequest(currentRequest, null); 210 211 // sendResponse may be called immediately, or later 212 requestProcessing = true; 213 handleRequest(currentRequest); 214 } 215 } 216 217 final void logRequest(HttpRequest request, HttpResponse response) 218 { 219 debug // avoid linewrap in terminal during development 220 enum DEBUG = true; 221 else 222 enum DEBUG = false; 223 224 if (log) log(([ 225 "", // align IP to tab 226 remoteAddressStr(request), 227 response ? text(cast(ushort)response.status) : "-", 228 request ? format("%9.2f ms", request.age.total!"usecs" / 1000f) : "-", 229 request ? request.method : "-", 230 request ? formatLocalAddress(request) ~ request.resource : "-", 231 response ? response.headers.get("Content-Type", "-") : "-", 232 ] ~ (DEBUG ? [] : [ 233 request ? request.headers.get("Referer", "-") : "-", 234 request ? request.headers.get("User-Agent", "-") : "-", 235 ])).join("\t")); 236 } 237 238 abstract string formatLocalAddress(HttpRequest r); 239 240 /// Idle connections are those which can be closed when the server 241 /// is shutting down. 242 final @property bool idle() 243 { 244 // Technically, with a persistent connection, we never know if 245 // there is a request on the wire on the way to us which we 246 // haven't received yet, so it's not possible to truly know 247 // when the connection is idle and can be safely closed. 248 // However, we do have the ability to do that for 249 // non-persistent connections - assume that a connection is 250 // never idle until we receive (and process) the first 251 // request. Therefore, in deployments where clients require 252 // that an outstanding request is always processed before the 253 // server is shut down, non-persistent connections can be used 254 // (i.e. no attempt to reuse `HttpClient`) to achieve this. 255 if (firstRequest) 256 return false; 257 258 if (requestProcessing) 259 return false; 260 261 foreach (datum; inBuffer) 262 if (datum.length) 263 return false; 264 265 return true; 266 } 267 268 /// Send the given HTTP response, and do nothing else. 269 final void writeResponse(HttpResponse response) 270 { 271 assert(response.status != 0); 272 273 if (currentRequest) 274 { 275 if (optimizeResponses) 276 response.optimizeData(currentRequest.headers); 277 if (satisfyRangeRequests) 278 response.sliceData(currentRequest.headers); 279 } 280 281 if ("Content-Length" !in response.headers) 282 response.headers["Content-Length"] = text(response.data.bytes.length); 283 284 sendHeaders(response); 285 286 bool isHead = currentRequest ? currentRequest.method == "HEAD" : false; 287 if (response && response.data.length && !isHead) 288 sendData(response.data[]); 289 290 responseSize = response ? response.data.bytes.length : 0; 291 debug (HTTP) debugLog("Sent response (%d bytes data)", responseSize); 292 } 293 294 public: 295 /// Send the given HTTP response. 296 final void sendResponse(HttpResponse response) 297 { 298 requestProcessing = false; 299 if (!response) 300 { 301 debug (HTTP) debugLog("sendResponse(null) - generating dummy response"); 302 response = new HttpResponse(); 303 response.status = HttpStatusCode.InternalServerError; 304 response.statusMessage = HttpResponse.getStatusMessage(HttpStatusCode.InternalServerError); 305 response.data = DataVec(Data("Internal Server Error".asBytes)); 306 } 307 writeResponse(response); 308 309 closeResponse(); 310 311 logRequest(currentRequest, response); 312 } 313 314 /// Switch protocols. 315 /// If `response` is given, send that first. 316 /// Then, release the connection and return it. 317 final Upgrade upgrade(HttpResponse response = null) 318 { 319 requestProcessing = false; 320 if (response) 321 writeResponse(response); 322 323 conn.handleReadData = null; 324 conn.handleDisconnect = null; 325 326 Upgrade upgrade; 327 upgrade.conn = conn; 328 upgrade.initialData = move(inBuffer); 329 330 this.conn = null; 331 assert(!timeoutActive); 332 333 logRequest(currentRequest, response); 334 return upgrade; 335 } 336 337 struct Upgrade 338 { 339 IConnection conn; /// The connection. 340 341 /// Any data that came after the request. 342 /// It is almost surely part of the protocol being upgraded to, 343 /// so it should be parsed as such. 344 DataVec initialData; 345 } /// ditto 346 347 /// Send these headers only. 348 /// Low-level alternative to `sendResponse`. 349 final void sendHeaders(Headers headers, HttpStatusCode status, string statusMessage = null) 350 { 351 assert(status, "Unset status code"); 352 353 if (!statusMessage) 354 statusMessage = HttpResponse.getStatusMessage(status); 355 356 StringBuilder respMessage; 357 auto protocolVersion = currentRequest ? currentRequest.protocolVersion : "1.0"; 358 respMessage.put("HTTP/", protocolVersion, " "); 359 360 if (banner && "X-Powered-By" !in headers) 361 headers["X-Powered-By"] = banner; 362 363 if ("Date" !in headers) 364 headers["Date"] = httpTime(Clock.currTime()); 365 366 if ("Connection" !in headers) 367 { 368 if (persistent && protocolVersion=="1.0") 369 headers["Connection"] = "Keep-Alive"; 370 else 371 if (!persistent && protocolVersion=="1.1") 372 headers["Connection"] = "close"; 373 } 374 375 respMessage.put("%d %s\r\n".format(status, statusMessage)); 376 foreach (string header, string value; headers) 377 respMessage.put(header, ": ", value, "\r\n"); 378 379 debug (HTTP) debugLog("Response headers:\n> %s", respMessage.get().chomp().replace("\r\n", "\n> ")); 380 381 respMessage.put("\r\n"); 382 conn.send(Data(respMessage.get().asBytes)); 383 } 384 385 /// ditto 386 final void sendHeaders(HttpResponse response) 387 { 388 sendHeaders(response.headers, response.status, response.statusMessage); 389 } 390 391 /// Send this data only. 392 /// Headers should have already been sent. 393 /// Low-level alternative to `sendResponse`. 394 final void sendData(scope Data[] data) 395 { 396 conn.send(data); 397 } 398 399 /// Accept more requests on the same connection? 400 protected bool acceptMore() { return true; } 401 402 /// Finalize writing the response. 403 /// Headers and data should have already been sent. 404 /// Low-level alternative to `sendResponse`. 405 final void closeResponse() 406 { 407 firstRequest = false; 408 if (persistent && acceptMore) 409 { 410 // reset for next request 411 debug (HTTP) debugLog(" Waiting for next request."); 412 conn.handleReadData = &onNewRequest; 413 if (!timeoutActive) 414 { 415 // Give the client time to download large requests. 416 // Assume a minimal speed of 1kb/s. 417 if (timer) 418 timer.setIdleTimeout(timeout + (responseSize / 1024).seconds); 419 timeoutActive = true; 420 } 421 if (inBuffer.bytes.length) // a second request has been pipelined 422 { 423 debug (HTTP) debugLog("A second request has been pipelined: %d datums, %d bytes", inBuffer.length, inBuffer.bytes.length); 424 onNewRequest(Data()); 425 } 426 } 427 else 428 { 429 string reason = persistent ? "Server has been shut down" : "Non-persistent connection"; 430 debug (HTTP) debugLog(" Closing connection (%s).", reason); 431 conn.disconnect(reason); 432 } 433 } 434 435 /// Retrieve the remote address of the peer, as a string. 436 abstract @property string remoteAddressStr(HttpRequest r); 437 } 438 439 /// Basic unencrypted HTTP 1.0/1.1 server. 440 class HttpServer 441 { 442 enum defaultTimeout = 30.seconds; /// The default timeout used for incoming connections. 443 444 // public: 445 this(Duration timeout = defaultTimeout) 446 { 447 assert(timeout > Duration.zero); 448 this.timeout = timeout; 449 450 conn = new TcpServer(); 451 conn.handleClose = &onClose; 452 conn.handleAccept = &onAccept; 453 } /// 454 455 /// Listen on the given TCP address and port. 456 /// If port is 0, listen on a random available port. 457 /// Returns the port that the server is actually listening on. 458 ushort listen(ushort port, string addr = null) 459 { 460 port = conn.listen(port, addr); 461 if (log) 462 foreach (address; conn.localAddresses) 463 log("Listening on " ~ formatAddress(protocol, address) ~ " [" ~ to!string(address.addressFamily) ~ "]"); 464 return port; 465 } 466 467 /// Listen on the given addresses. 468 void listen(AddressInfo[] addresses) 469 { 470 conn.listen(addresses); 471 if (log) 472 foreach (address; conn.localAddresses) 473 log("Listening on " ~ formatAddress(protocol, address) ~ " [" ~ to!string(address.addressFamily) ~ "]"); 474 } 475 476 /// Get listen addresses. 477 @property Address[] localAddresses() { return conn.localAddresses; } 478 479 /// Stop listening, and close idle client connections. 480 void close() 481 { 482 debug(HTTP) stderr.writeln("Shutting down"); 483 if (log) log("Shutting down."); 484 conn.close(); 485 486 debug(HTTP) stderr.writefln("There still are %d active connections", connections.iterator.walkLength); 487 488 // Close idle connections 489 foreach (connection; connections.iterator.array) 490 if (connection.idle && connection.conn.state == ConnectionState.connected) 491 connection.conn.disconnect("HTTP server shutting down"); 492 } 493 494 /// Optional HTTP request log. 495 Logger log; 496 497 /// Single-ended doubly-linked list of active connections 498 SEDListContainer!HttpServerConnection connections; 499 500 /// Callback for when the socket was closed. 501 void delegate() handleClose; 502 /// Callback for an incoming request. 503 void delegate(HttpRequest request, HttpServerConnection conn) handleRequest; 504 505 /// What to send in the `"X-Powered-By"` header. 506 string banner = "ae.net.http.server (+https://github.com/CyberShadow/ae)"; 507 508 /// If set, the name of the header which will be used to obtain 509 /// the actual IP of the connecting peer. Useful when this 510 /// `HttpServer` is behind a reverse proxy. 511 string remoteIPHeader; 512 513 protected: 514 TcpServer conn; 515 Duration timeout; 516 517 void onClose() 518 { 519 if (handleClose) 520 handleClose(); 521 } 522 523 IConnection adaptConnection(IConnection transport) 524 { 525 return transport; 526 } 527 528 @property string protocol() { return "http"; } 529 530 void onAccept(TcpConnection incoming) 531 { 532 try 533 new HttpServerConnection(this, incoming, adaptConnection(incoming), protocol); 534 catch (Exception e) 535 { 536 if (log) 537 log("Error accepting connection: " ~ e.msg); 538 if (incoming.state == ConnectionState.connected) 539 incoming.disconnect(); 540 } 541 } 542 } 543 544 /** 545 HTTPS server. Set SSL parameters on ctx after instantiation. 546 547 Example: 548 --- 549 auto s = new HttpsServer(); 550 s.ctx.enableDH(4096); 551 s.ctx.enableECDH(); 552 s.ctx.setCertificate("server.crt"); 553 s.ctx.setPrivateKey("server.key"); 554 --- 555 */ 556 class HttpsServer : HttpServer 557 { 558 SSLContext ctx; /// The SSL context. 559 560 this() 561 { 562 ctx = ssl.createContext(SSLContext.Kind.server); 563 } /// 564 565 protected: 566 override @property string protocol() { return "https"; } 567 568 override IConnection adaptConnection(IConnection transport) 569 { 570 return ssl.createAdapter(ctx, transport); 571 } 572 } 573 574 /// Standard socket-based HTTP server connection. 575 final class HttpServerConnection : BaseHttpServerConnection 576 { 577 SocketConnection socket; /// The socket transport. 578 HttpServer server; /// `HttpServer` owning this connection. 579 /// Cached local and remote addresses. 580 Address localAddress, remoteAddress; 581 582 mixin DListLink; 583 584 /// Retrieves the remote peer address, honoring `remoteIPHeader` if set. 585 override @property string remoteAddressStr(HttpRequest r) 586 { 587 if (server.remoteIPHeader) 588 { 589 if (r) 590 if (auto p = server.remoteIPHeader in r.headers) 591 return (*p).split(",")[$ - 1]; 592 593 return "[local:" ~ remoteAddress.toAddrString() ~ "]"; 594 } 595 596 return remoteAddress.toAddrString(); 597 } 598 599 protected: 600 this(HttpServer server, SocketConnection socket, IConnection c, string protocol = "http") 601 { 602 this.server = server; 603 this.socket = socket; 604 this.log = server.log; 605 this.protocol = protocol; 606 this.banner = server.banner; 607 this.timeout = server.timeout; 608 this.handleRequest = (HttpRequest r) => server.handleRequest(r, this); 609 this.localAddress = socket.localAddress; 610 this.remoteAddress = socket.remoteAddress; 611 612 super(c); 613 614 server.connections.pushFront(this); 615 } 616 617 override void onDisconnect(string reason, DisconnectType type) 618 { 619 super.onDisconnect(reason, type); 620 server.connections.remove(this); 621 } 622 623 override bool acceptMore() { return server.conn.isListening; } 624 override string formatLocalAddress(HttpRequest r) { return formatAddress(protocol, localAddress, r.host, r.port); } 625 } 626 627 /// `BaseHttpServerConnection` implementation with files, allowing to 628 /// e.g. read a request from standard input and write the response to 629 /// standard output. 630 version (Posix) 631 class FileHttpServerConnection : BaseHttpServerConnection 632 { 633 this(File input = stdin, File output = stdout, string protocol = "stdin") 634 { 635 this.protocol = protocol; 636 637 auto c = new Duplex( 638 new FileConnection(input.fileno), 639 new FileConnection(output.fileno), 640 ); 641 642 super(c); 643 } /// 644 645 override @property string remoteAddressStr(HttpRequest r) { return "-"; } /// Stub. 646 647 protected: 648 import std.stdio : File, stdin, stdout; 649 650 string protocol; 651 652 override string formatLocalAddress(HttpRequest r) { return protocol ~ "://"; } 653 } 654 655 /// Formats a remote address for logging. 656 string formatAddress(string protocol, Address address, string vhost = null, ushort logPort = 0) 657 { 658 string addr = address.toAddrString(); 659 string port = 660 address.addressFamily == AddressFamily.UNIX ? null : 661 logPort ? text(logPort) : 662 address.toPortString(); 663 return protocol ~ "://" ~ 664 (vhost ? vhost : addr == "0.0.0.0" || addr == "::" ? "*" : addr.contains(":") ? "[" ~ addr ~ "]" : addr) ~ 665 (port is null || port == "80" ? "" : ":" ~ port); 666 } 667 668 version (unittest) import ae.net.http.client; 669 version (unittest) import ae.net.http.responseex; 670 unittest 671 { 672 int[] replies; 673 int closeAfter; 674 675 // Sum "a" from GET and "b" from POST 676 auto s = new HttpServer; 677 s.handleRequest = (HttpRequest request, HttpServerConnection conn) { 678 auto get = request.urlParameters; 679 auto post = request.decodePostData(); 680 auto response = new HttpResponseEx; 681 auto result = to!int(get["a"]) + to!int(post["b"]); 682 replies ~= result; 683 conn.sendResponse(response.serveJson(result)); 684 if (--closeAfter == 0) 685 s.close(); 686 }; 687 688 // Test server, client, parameter encoding 689 replies = null; 690 closeAfter = 1; 691 auto port = s.listen(0, "127.0.0.1"); 692 httpPost("http://127.0.0.1:" ~ to!string(port) ~ "/?" ~ encodeUrlParameters(["a":"2"]), UrlParameters(["b":"3"]), (string s) { assert(s=="5"); }, null); 693 socketManager.loop(); 694 695 // Test pipelining, protocol errors 696 replies = null; 697 closeAfter = 2; 698 port = s.listen(0, "127.0.0.1"); 699 TcpConnection c = new TcpConnection; 700 c.handleConnect = { 701 c.send(Data(( 702 "GET /?a=123456 HTTP/1.1 703 Content-length: 8 704 Content-type: application/x-www-form-urlencoded 705 706 b=654321" ~ 707 "GET /derp HTTP/1.1 708 Content-length: potato 709 710 " ~ 711 "GET /?a=1234567 HTTP/1.1 712 Content-length: 9 713 Content-type: application/x-www-form-urlencoded 714 715 b=7654321").asBytes)); 716 c.disconnect(); 717 }; 718 c.connect("127.0.0.1", port); 719 720 socketManager.loop(); 721 722 assert(replies == [777777, 8888888]); 723 724 // Test bad headers 725 s.handleRequest = (HttpRequest request, HttpServerConnection conn) { 726 auto response = new HttpResponseEx; 727 conn.sendResponse(response.serveText("OK")); 728 if (--closeAfter == 0) 729 s.close(); 730 }; 731 closeAfter = 1; 732 733 port = s.listen(0, "127.0.0.1"); 734 c = new TcpConnection; 735 c.handleConnect = { 736 c.send(Data("\n\n\n\n\n".asBytes)); 737 c.disconnect(); 738 739 // Now send a valid request to end the loop 740 c = new TcpConnection; 741 c.handleConnect = { 742 c.send(Data("GET / HTTP/1.0\n\n".asBytes)); 743 c.disconnect(); 744 }; 745 c.connect("127.0.0.1", port); 746 }; 747 c.connect("127.0.0.1", port); 748 749 socketManager.loop(); 750 751 /+ 752 void testFile(string fn) 753 { 754 std.file.write(fn, "42"); 755 s.handleRequest = (HttpRequest request, HttpServerConnection conn) { 756 auto response = new HttpResponseEx; 757 conn.sendResponse(response.serveFile(request.resource[1..$], "")); 758 if (--closeAfter == 0) 759 s.close(); 760 }; 761 port = s.listen(0, "127.0.0.1"); 762 closeAfter = 1; 763 httpGet("http://127.0.0.1:" ~ to!string(port) ~ "/" ~ fn, (string s) { assert(s=="42"); }, null); 764 socketManager.loop(); 765 std.file.remove(fn); 766 } 767 768 testFile("http-test.bin"); 769 testFile("http-test.txt"); 770 +/ 771 } 772 773 // Test form-data 774 unittest 775 { 776 // Temporarily disabled due to DMD regression 777 // https://issues.dlang.org/show_bug.cgi?id=24050 778 { 779 import std.process : environment; 780 if ("BUILDKITE" in environment) 781 return; 782 } 783 784 bool ok; 785 auto s = new HttpServer; 786 s.handleRequest = (HttpRequest request, HttpServerConnection conn) { 787 auto post = request.decodePostData(); 788 assert(post["a"] == "b"); 789 assert(post["c"] == "d"); 790 assert(post["e"] == "f"); 791 ok = true; 792 conn.conn.disconnect(); 793 s.close(); 794 }; 795 auto port = s.listen(0, "127.0.0.1"); 796 797 TcpConnection c = new TcpConnection; 798 c.handleConnect = { 799 c.send(Data((q"EOF 800 POST / HTTP/1.1 801 Host: google.com 802 User-Agent: curl/8.1.2 803 Accept: */* 804 Content-Length: 319 805 Content-Type: multipart/form-data; boundary=------------------------f7d0ffeae587957a 806 807 --------------------------f7d0ffeae587957a 808 Content-Disposition: form-data; name="a" 809 810 b 811 --------------------------f7d0ffeae587957a 812 Content-Disposition: form-data; name="c" 813 814 d 815 --------------------------f7d0ffeae587957a 816 Content-Disposition: form-data; name="e" 817 818 f 819 --------------------------f7d0ffeae587957a-- 820 EOF".replace("\n", "\r\n")).asBytes)); 821 c.disconnect(); 822 }; 823 c.connect("127.0.0.1", port); 824 825 socketManager.loop(); 826 827 assert(ok); 828 }