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.ssl; 30 import ae.sys.data; 31 debug import std.stdio; 32 33 public import ae.net.http.common; 34 35 36 class HttpClient 37 { 38 private: 39 ClientSocket conn; 40 Data[] inBuffer; 41 42 HttpRequest currentRequest; 43 44 HttpResponse currentResponse; 45 size_t expect; 46 47 protected: 48 void onConnect(ClientSocket sender) 49 { 50 string reqMessage = currentRequest.method ~ " "; 51 if (currentRequest.proxy !is null) { 52 reqMessage ~= "http://" ~ currentRequest.host; 53 if (compat || currentRequest.port != 80) 54 reqMessage ~= format(":%d", currentRequest.port); 55 } 56 reqMessage ~= currentRequest.resource ~ " HTTP/1.0\r\n"; 57 58 if (!("User-Agent" in currentRequest.headers)) 59 currentRequest.headers["User-Agent"] = agent; 60 if (!compat) { 61 if (!("Accept-Encoding" in currentRequest.headers)) 62 currentRequest.headers["Accept-Encoding"] = "gzip, deflate, *;q=0"; 63 if (currentRequest.data) 64 currentRequest.headers["Content-Length"] = to!string(currentRequest.data.bytes.length); 65 } else { 66 if (!("Pragma" in currentRequest.headers)) 67 currentRequest.headers["Pragma"] = "No-Cache"; 68 } 69 foreach (string header, string value; currentRequest.headers) 70 reqMessage ~= header ~ ": " ~ value ~ "\r\n"; 71 72 reqMessage ~= "\r\n"; 73 74 conn.send(Data(reqMessage)); 75 conn.send(currentRequest.data); 76 } 77 78 void onNewResponse(ClientSocket sender, Data data) 79 { 80 try 81 { 82 inBuffer ~= data; 83 conn.markNonIdle(); 84 85 string statusLine; 86 Headers headers; 87 88 if (!parseHeaders(inBuffer, statusLine, headers)) 89 return; 90 91 currentResponse = new HttpResponse; 92 currentResponse.parseStatusLine(statusLine); 93 currentResponse.headers = headers; 94 95 expect = size_t.max; 96 if ("Content-Length" in currentResponse.headers) 97 expect = to!size_t(strip(currentResponse.headers["Content-Length"])); 98 99 if (expect > inBuffer.bytes.length) 100 conn.handleReadData = &onContinuation; 101 else 102 { 103 currentResponse.data = inBuffer.bytes[0 .. expect]; 104 conn.disconnect("All data read"); 105 } 106 } 107 catch (Exception e) 108 { 109 conn.disconnect(e.msg, DisconnectType.Error); 110 } 111 } 112 113 void onContinuation(ClientSocket sender, Data data) 114 { 115 inBuffer ~= data; 116 sender.markNonIdle(); 117 118 if (expect!=size_t.max && inBuffer.length >= expect) 119 { 120 currentResponse.data = inBuffer[0 .. expect]; 121 conn.disconnect("All data read"); 122 } 123 } 124 125 void onDisconnect(ClientSocket sender, string reason, DisconnectType type) 126 { 127 if (type == DisconnectType.Error) 128 currentResponse = null; 129 else 130 if (currentResponse) 131 currentResponse.data = inBuffer; 132 133 if (handleResponse) 134 handleResponse(currentResponse, reason); 135 136 currentRequest = null; 137 currentResponse = null; 138 inBuffer.destroy(); 139 expect = -1; 140 conn.handleReadData = null; 141 } 142 143 ClientSocket createSocket() 144 { 145 return new ClientSocket(); 146 } 147 148 public: 149 string agent = "ae.net.http.client (+https://github.com/CyberShadow/ae)"; 150 bool compat = false; 151 string[] cookies; 152 153 public: 154 this(Duration timeout = 30.seconds) 155 { 156 assert(timeout > Duration.zero); 157 conn = createSocket(); 158 conn.setIdleTimeout(timeout); 159 conn.handleConnect = &onConnect; 160 conn.handleDisconnect = &onDisconnect; 161 } 162 163 void request(HttpRequest request) 164 { 165 //debug writefln("New HTTP request: %s", request.url); 166 currentRequest = request; 167 currentResponse = null; 168 conn.handleReadData = &onNewResponse; 169 expect = 0; 170 if (request.proxy !is null) 171 conn.connect(request.proxyHost, request.proxyPort); 172 else 173 conn.connect(request.host, request.port); 174 } 175 176 bool connected() 177 { 178 return currentRequest !is null; 179 } 180 181 public: 182 // Provide the following callbacks 183 void delegate(HttpResponse response, string disconnectReason) handleResponse; 184 } 185 186 class HttpsClient : HttpClient 187 { 188 override ClientSocket createSocket() 189 { 190 return sslSocketFactory(); 191 } 192 193 this(Duration timeout = 30.seconds) { super(timeout); } 194 } 195 196 /// Asynchronous HTTP request 197 void httpRequest(HttpRequest request, void delegate(HttpResponse response, string disconnectReason) responseHandler) 198 { 199 HttpClient client; 200 if (request.protocol == "https") 201 client = new HttpsClient; 202 else 203 client = new HttpClient; 204 205 client.handleResponse = responseHandler; 206 client.request(request); 207 } 208 209 /// ditto 210 void httpRequest(HttpRequest request, void delegate(Data) resultHandler, void delegate(string) errorHandler, int redirectCount = 0) 211 { 212 void responseHandler(HttpResponse response, string disconnectReason) 213 { 214 if (!response) 215 if (errorHandler) 216 errorHandler(disconnectReason); 217 else 218 throw new Exception(disconnectReason); 219 else 220 if (response.status >= 300 && response.status < 400 && "Location" in response.headers) 221 { 222 if (redirectCount == 15) 223 throw new Exception("HTTP redirect loop: " ~ request.url); 224 request.resource = applyRelativeURL(request.url, response.headers["Location"]); 225 if (response.status == HttpStatusCode.SeeOther) 226 { 227 request.method = "GET"; 228 request.data = null; 229 } 230 httpRequest(request, resultHandler, errorHandler, redirectCount+1); 231 } 232 else 233 if (errorHandler) 234 try 235 resultHandler(response.getContent()); 236 catch (Exception e) 237 errorHandler(e.msg); 238 else 239 resultHandler(response.getContent()); 240 } 241 242 httpRequest(request, &responseHandler); 243 } 244 245 /// ditto 246 void httpGet(string url, void delegate(Data) resultHandler, void delegate(string) errorHandler) 247 { 248 auto request = new HttpRequest; 249 request.resource = url; 250 httpRequest(request, resultHandler, errorHandler); 251 } 252 253 /// ditto 254 void httpGet(string url, void delegate(string) resultHandler, void delegate(string) errorHandler) 255 { 256 httpGet(url, 257 (Data data) 258 { 259 auto result = (cast(char[])data.contents).idup; 260 std.utf.validate(result); 261 resultHandler(result); 262 }, 263 errorHandler); 264 } 265 266 /// ditto 267 void httpPost(string url, string[string] vars, void delegate(string) resultHandler, void delegate(string) errorHandler) 268 { 269 auto request = new HttpRequest; 270 request.resource = url; 271 request.method = "POST"; 272 request.headers["Content-Type"] = "application/x-www-form-urlencoded"; 273 request.data = [Data(encodeUrlParameters(vars))]; 274 httpRequest(request, 275 (Data data) 276 { 277 auto result = (cast(char[])data.contents).idup; 278 std.utf.validate(result); 279 resultHandler(result); 280 }, 281 errorHandler); 282 }