1 /** 2 * A simple HTTP client. 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 <vladimir@thecybershadow.net> 13 * Vincent Povirk <madewokherd@gmail.com> 14 * Simon Arlott 15 */ 16 17 module ae.net.http.client; 18 19 import std.string; 20 import std.conv; 21 import std.datetime; 22 import std.uri; 23 import std.utf; 24 25 import ae.net.asockets; 26 import ae.net.ietf.headers; 27 import ae.net.ietf.headerparse; 28 import ae.net.ietf.url; 29 import ae.net.ssl; 30 import ae.utils.array : toArray; 31 import ae.utils.exception : CaughtException; 32 import ae.sys.data; 33 debug(HTTP) import std.stdio : stderr; 34 35 public import ae.net.http.common; 36 37 class HttpClient 38 { 39 private: 40 Connector connector; // Bottom-level transport factory. 41 TimeoutAdapter timer; // Timeout adapter. 42 IConnection conn; // Top-level abstract connection. Reused for new connections. 43 44 Data[] inBuffer; 45 46 protected: 47 HttpRequest currentRequest; 48 49 HttpResponse currentResponse; 50 size_t expect; 51 52 void onConnect() 53 { 54 sendRequest(currentRequest); 55 } 56 57 void sendRequest(HttpRequest request) 58 { 59 if ("User-Agent" !in request.headers && agent) 60 request.headers["User-Agent"] = agent; 61 if (!compat) { 62 if ("Accept-Encoding" !in request.headers) 63 request.headers["Accept-Encoding"] = "gzip, deflate, *;q=0"; 64 if (request.data) 65 request.headers["Content-Length"] = to!string(request.data.bytes.length); 66 } else { 67 if ("Pragma" !in request.headers) 68 request.headers["Pragma"] = "No-Cache"; 69 } 70 if ("Connection" !in request.headers) 71 request.headers["Connection"] = keepAlive ? "keep-alive" : "close"; 72 73 sendRawRequest(request); 74 } 75 76 void sendRawRequest(HttpRequest request) 77 { 78 string reqMessage = request.method ~ " "; 79 if (request.proxy !is null) { 80 reqMessage ~= "http://" ~ request.host; 81 if (compat || request.port != 80) 82 reqMessage ~= format(":%d", request.port); 83 } 84 reqMessage ~= request.resource ~ " HTTP/1.0\r\n"; 85 86 foreach (string header, string value; request.headers) 87 if (value !is null) 88 reqMessage ~= header ~ ": " ~ value ~ "\r\n"; 89 90 reqMessage ~= "\r\n"; 91 debug(HTTP) 92 { 93 stderr.writefln("Sending request:"); 94 foreach (line; reqMessage.split("\r\n")) 95 stderr.writeln("> ", line); 96 if (request.data) 97 stderr.writefln("} (%d bytes data follow)", request.data.bytes.length); 98 } 99 100 conn.send(Data(reqMessage)); 101 conn.send(request.data); 102 } 103 104 void onNewResponse(Data data) 105 { 106 try 107 { 108 inBuffer ~= data; 109 if (timer) 110 timer.markNonIdle(); 111 112 string statusLine; 113 Headers headers; 114 115 debug(HTTP) auto oldData = inBuffer.dup; 116 117 if (!parseHeaders(inBuffer, statusLine, headers)) 118 return; 119 120 debug(HTTP) 121 { 122 stderr.writefln("Got response:"); 123 auto reqMessage = cast(string)oldData.bytes[0..oldData.bytes.length-inBuffer.bytes.length].joinToHeap(); 124 foreach (line; reqMessage.split("\r\n")) 125 stderr.writeln("< ", line); 126 } 127 128 currentResponse = new HttpResponse; 129 currentResponse.parseStatusLine(statusLine); 130 currentResponse.headers = headers; 131 132 onHeadersReceived(); 133 } 134 catch (CaughtException e) 135 { 136 if (conn.state == ConnectionState.connected) 137 conn.disconnect(e.msg.length ? e.msg : e.classinfo.name, DisconnectType.error); 138 else 139 throw new Exception("Unhandled exception after connection was closed: " ~ e.msg, e); 140 } 141 } 142 143 void onHeadersReceived() 144 { 145 expect = size_t.max; 146 if ("Content-Length" in currentResponse.headers) 147 expect = to!size_t(strip(currentResponse.headers["Content-Length"])); 148 149 if (inBuffer.bytes.length < expect) 150 { 151 onData(inBuffer); 152 conn.handleReadData = &onContinuation; 153 } 154 else 155 { 156 onData(inBuffer.bytes[0 .. expect]); // TODO: pipelining 157 onDone(); 158 } 159 160 inBuffer.destroy(); 161 } 162 163 void onData(Data[] data) 164 { 165 currentResponse.data ~= data; 166 } 167 168 void onContinuation(Data data) 169 { 170 onData(data.toArray); 171 if (timer) 172 timer.markNonIdle(); 173 174 auto received = currentResponse.data.bytes.length; 175 if (expect!=size_t.max && received >= expect) 176 { 177 inBuffer = currentResponse.data.bytes[expect..received]; 178 currentResponse.data = currentResponse.data.bytes[0..expect]; 179 onDone(); 180 } 181 } 182 183 void onDone() 184 { 185 if (keepAlive) 186 processResponse(); 187 else 188 conn.disconnect("All data read"); 189 } 190 191 void processResponse(string reason = "All data read") 192 { 193 auto response = currentResponse; 194 195 currentRequest = null; 196 currentResponse = null; 197 expect = -1; 198 conn.handleReadData = null; 199 200 if (handleResponse) 201 handleResponse(response, reason); 202 } 203 204 void onDisconnect(string reason, DisconnectType type) 205 { 206 if (type == DisconnectType.error) 207 currentResponse = null; 208 209 if (currentRequest) 210 processResponse(reason); 211 } 212 213 IConnection adaptConnection(IConnection conn) 214 { 215 return conn; 216 } 217 218 public: 219 string agent = "ae.net.http.client (+https://github.com/CyberShadow/ae)"; 220 bool compat = false; 221 bool keepAlive = false; 222 string[] cookies; 223 224 public: 225 this(Duration timeout = 30.seconds, Connector connector = new TcpConnector) 226 { 227 assert(timeout >= Duration.zero); 228 229 this.connector = connector; 230 IConnection c = connector.getConnection(); 231 232 c = adaptConnection(c); 233 234 if (timeout > Duration.zero) 235 { 236 timer = new TimeoutAdapter(c); 237 timer.setIdleTimeout(timeout); 238 c = timer; 239 } 240 241 conn = c; 242 conn.handleConnect = &onConnect; 243 conn.handleDisconnect = &onDisconnect; 244 } 245 246 void request(HttpRequest request) 247 { 248 //debug writefln("New HTTP request: %s", request.url); 249 currentRequest = request; 250 currentResponse = null; 251 conn.handleReadData = &onNewResponse; 252 expect = 0; 253 254 if (conn.state != ConnectionState.disconnected) 255 { 256 assert(conn.state == ConnectionState.connected, "Attempting a HTTP request on a %s connection".format(conn.state)); 257 assert(keepAlive, "Attempting a second HTTP request on a connected non-keepalive connection"); 258 sendRequest(request); 259 } 260 else 261 { 262 if (request.proxy !is null) 263 connector.connect(request.proxyHost, request.proxyPort); 264 else 265 connector.connect(request.host, request.port); 266 } 267 } 268 269 bool connected() 270 { 271 if (currentRequest !is null) 272 return true; 273 if (keepAlive && conn.state == ConnectionState.connected) 274 return true; 275 return false; 276 } 277 278 void disconnect(string reason = IConnection.defaultDisconnectReason) 279 { 280 conn.disconnect(reason); 281 } 282 283 public: 284 // Provide the following callbacks 285 void delegate(HttpResponse response, string disconnectReason) handleResponse; 286 } 287 288 class HttpsClient : HttpClient 289 { 290 SSLContext ctx; 291 SSLAdapter adapter; 292 293 this(Duration timeout = 30.seconds) 294 { 295 ctx = ssl.createContext(SSLContext.Kind.client); 296 super(timeout); 297 } 298 299 override IConnection adaptConnection(IConnection conn) 300 { 301 adapter = ssl.createAdapter(ctx, conn); 302 return adapter; 303 } 304 305 override void request(HttpRequest request) 306 { 307 super.request(request); 308 if (conn.state == ConnectionState.connecting) 309 adapter.setHostName(request.host); 310 } 311 } 312 313 // Experimental for now 314 class Connector 315 { 316 abstract IConnection getConnection(); 317 abstract void connect(string host, ushort port); 318 } 319 320 // ditto 321 class TcpConnector : Connector 322 { 323 protected TcpConnection conn; 324 325 this() 326 { 327 conn = new TcpConnection(); 328 } 329 330 override IConnection getConnection() 331 { 332 return conn; 333 } 334 335 override void connect(string host, ushort port) 336 { 337 conn.connect(host, port); 338 } 339 } 340 341 // ditto 342 version(Posix) 343 class UnixConnector : TcpConnector 344 { 345 string path; 346 347 this(string path) 348 { 349 this.path = path; 350 } 351 352 override void connect(string host, ushort port) 353 { 354 import std.socket; 355 auto addr = new UnixAddress(path); 356 conn.connect([AddressInfo(AddressFamily.UNIX, SocketType.STREAM, cast(ProtocolType)0, addr, path)]); 357 } 358 } 359 360 /// Asynchronous HTTP request 361 void httpRequest(HttpRequest request, void delegate(HttpResponse response, string disconnectReason) responseHandler) 362 { 363 HttpClient client; 364 if (request.protocol == "https") 365 client = new HttpsClient; 366 else 367 client = new HttpClient; 368 369 client.handleResponse = responseHandler; 370 client.request(request); 371 } 372 373 /// ditto 374 void httpRequest(HttpRequest request, void delegate(Data) resultHandler, void delegate(string) errorHandler, int redirectCount = 0) 375 { 376 void responseHandler(HttpResponse response, string disconnectReason) 377 { 378 if (!response) 379 if (errorHandler) 380 errorHandler(disconnectReason); 381 else 382 throw new Exception(disconnectReason); 383 else 384 if (response.status >= 300 && response.status < 400 && "Location" in response.headers) 385 { 386 if (redirectCount == 15) 387 throw new Exception("HTTP redirect loop: " ~ request.url); 388 request.resource = applyRelativeURL(request.url, response.headers["Location"]); 389 if (response.status == HttpStatusCode.SeeOther) 390 { 391 request.method = "GET"; 392 request.data = null; 393 } 394 httpRequest(request, resultHandler, errorHandler, redirectCount+1); 395 } 396 else 397 if (errorHandler) 398 try 399 resultHandler(response.getContent()); 400 catch (Exception e) 401 errorHandler(e.msg); 402 else 403 resultHandler(response.getContent()); 404 } 405 406 httpRequest(request, &responseHandler); 407 } 408 409 /// ditto 410 void httpGet(string url, void delegate(HttpResponse response, string disconnectReason) responseHandler) 411 { 412 httpRequest(new HttpRequest(url), responseHandler); 413 } 414 415 /// ditto 416 void httpGet(string url, void delegate(Data) resultHandler, void delegate(string) errorHandler) 417 { 418 httpRequest(new HttpRequest(url), resultHandler, errorHandler); 419 } 420 421 /// ditto 422 void httpGet(string url, void delegate(string) resultHandler, void delegate(string) errorHandler) 423 { 424 httpGet(url, 425 (Data data) 426 { 427 auto result = (cast(char[])data.contents).idup; 428 std.utf.validate(result); 429 resultHandler(result); 430 }, 431 errorHandler); 432 } 433 434 /// ditto 435 void httpPost(string url, Data[] postData, string contentType, void delegate(Data) resultHandler, void delegate(string) errorHandler) 436 { 437 auto request = new HttpRequest; 438 request.resource = url; 439 request.method = "POST"; 440 if (contentType) 441 request.headers["Content-Type"] = contentType; 442 request.data = postData; 443 httpRequest(request, resultHandler, errorHandler); 444 } 445 446 /// ditto 447 void httpPost(string url, Data[] postData, string contentType, void delegate(string) resultHandler, void delegate(string) errorHandler) 448 { 449 httpPost(url, postData, contentType, 450 (Data data) 451 { 452 auto result = (cast(char[])data.contents).idup; 453 std.utf.validate(result); 454 resultHandler(result); 455 }, 456 errorHandler); 457 } 458 459 /// ditto 460 void httpPost(string url, UrlParameters vars, void delegate(string) resultHandler, void delegate(string) errorHandler) 461 { 462 return httpPost(url, [Data(encodeUrlParameters(vars))], "application/x-www-form-urlencoded", resultHandler, errorHandler); 463 } 464 465 // http://d.puremagic.com/issues/show_bug.cgi?id=7016 466 version (unittest) 467 { 468 static import ae.net.http.server; 469 static import ae.net.http.responseex; 470 } 471 472 unittest 473 { 474 import ae.net.http.server; 475 import ae.net.http.responseex; 476 477 void test(bool keepAlive) 478 { 479 auto s = new HttpServer; 480 s.handleRequest = (HttpRequest request, HttpServerConnection conn) { 481 auto response = new HttpResponseEx; 482 conn.sendResponse(response.serveText("Hello!")); 483 }; 484 auto port = s.listen(0, "127.0.0.1"); 485 486 auto c = new HttpClient; 487 c.keepAlive = keepAlive; 488 auto r = new HttpRequest("http://127.0.0.1:" ~ to!string(port)); 489 int count; 490 c.handleResponse = 491 (HttpResponse response, string disconnectReason) 492 { 493 assert(response, "HTTP server error"); 494 assert(cast(string)response.getContent.toHeap == "Hello!"); 495 if (++count == 5) 496 { 497 s.close(); 498 if (c.connected) 499 c.disconnect(); 500 } 501 else 502 c.request(r); 503 }; 504 c.request(r); 505 506 socketManager.loop(); 507 508 assert(count == 5); 509 } 510 511 test(false); 512 test(true); 513 }