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 <ae@cy.md>
13  *   Vincent Povirk <madewokherd@gmail.com>
14  *   Simon Arlott
15  */
16 
17 module ae.net.http.client;
18 
19 import std.algorithm.mutation : move, swap;
20 import std.exception : enforce;
21 import std.string;
22 import std.conv;
23 import std.datetime;
24 import std.uri;
25 import std.utf;
26 
27 import ae.net.asockets;
28 import ae.net.ietf.headers;
29 import ae.net.ietf.headerparse;
30 import ae.net.ietf.url;
31 import ae.net.ssl;
32 import ae.utils.array : toArray, shift;
33 import ae.utils.exception : CaughtException;
34 import ae.sys.data;
35 
36 debug(HTTP_CLIENT) debug = HTTP;
37 debug(HTTP) import std.stdio : stderr;
38 
39 public import ae.net.http.common;
40 
41 /// Implements a HTTP client connection to a single server.
42 class HttpClient
43 {
44 protected:
45 	Connector connector;  // Bottom-level transport factory.
46 	TimeoutAdapter timer; // Timeout adapter.
47 	IConnection conn;     // Top-level abstract connection. Reused for new connections.
48 
49 	HttpRequest[] requestQueue; // Requests that have been enqueued to send after the connection is established.
50 
51 	HttpResponse currentResponse; // Response to the currently-processed request.
52 	ulong sentRequests, receivedResponses; // Used to know when we're still waiting for something.
53 
54 	DataVec headerBuffer; // Received but un-parsed headers
55 	size_t expect;    // How much data do we expect to receive in the current request (size_t.max if until disconnect)
56 
57 	/// Connect to a request's destination.
58 	void connect(HttpRequest request)
59 	{
60 		assert(conn.state == ConnectionState.disconnected);
61 		if (request.proxy !is null)
62 			connector.connect(request.proxyHost, request.proxyPort);
63 		else
64 			connector.connect(request.host, request.port);
65 		assert(conn.state == ConnectionState.connecting);
66 
67 		// We must install a data read handler to indicate that we want to receive readable events.
68 		// Though, this isn't going to be actually called.
69 		// TODO: this should probably be fixed in OpenSSLAdapter instead.
70 		conn.handleReadData = (Data _/*data*/) { assert(false); };
71 	}
72 
73 	/// Called when the underlying connection (TCP, TLS...) is established.
74 	void onConnect()
75 	{
76 		onIdle();
77 	}
78 
79 	/// Called when we're ready to send a request.
80 	void onIdle()
81 	{
82 		assert(isIdle);
83 
84 		if (pipelining)
85 		{
86 			assert(keepAlive, "keepAlive is required for pipelining");
87 			// Pipeline all queued requests
88 			while (requestQueue.length)
89 				sendRequest(requestQueue.shift);
90 		}
91 		else
92 		{
93 			// One request at a time
94 			if (requestQueue.length)
95 				sendRequest(requestQueue.shift);
96 		}
97 
98 		expectResponse();
99 	}
100 
101 	/// Returns true when we are connected but not waiting for anything.
102 	/// Requests can always be sent immediately when this is true.
103 	bool isIdle()
104 	{
105 		if (conn.state == ConnectionState.connected && sentRequests == receivedResponses)
106 		{
107 			assert(!currentResponse);
108 			return true;
109 		}
110 		return false;
111 	}
112 
113 	/// Encode and send a request (headers and body) to the connection.
114 	/// Has no other side effects other than incrementing `sentRequests`.
115 	void sendRequest(HttpRequest request)
116 	{
117 		string reqMessage = request.method ~ " ";
118 		if (request.proxy !is null) {
119 			reqMessage ~= "http://" ~ request.host;
120 			if (request.port != 80)
121 				reqMessage ~= format(":%d", request.port);
122 		}
123 		reqMessage ~= request.resource ~ " HTTP/1.0\r\n";
124 
125 		foreach (string header, string value; request.headers)
126 			if (value !is null)
127 				reqMessage ~= header ~ ": " ~ value ~ "\r\n";
128 
129 		reqMessage ~= "\r\n";
130 		debug(HTTP)
131 		{
132 			stderr.writefln("Sending request:");
133 			foreach (line; reqMessage.split("\r\n"))
134 				stderr.writeln("> ", line);
135 			if (request.data)
136 				stderr.writefln("} (%d bytes data follow)", request.data.bytes.length);
137 		}
138 
139 		conn.send(Data(reqMessage));
140 		conn.send(request.data[]);
141 		sentRequests++;
142 	}
143 
144 	/// Called to set up the client to be ready to receive a response.
145 	void expectResponse()
146 	{
147 		//assert(conn.handleReadData is null);
148 		if (receivedResponses < sentRequests)
149 		{
150 			conn.handleReadData = &onNewResponse;
151 			expect = 0;
152 		}
153 	}
154 
155 	/// Received data handler used while we are receiving headers.
156 	void onNewResponse(Data data)
157 	{
158 		if (timer)
159 			timer.markNonIdle();
160 
161 		onHeaderData(data.toArray);
162 	}
163 
164 	/// Called when we've received some data from the response headers.
165 	void onHeaderData(scope Data[] data)
166 	{
167 		try
168 		{
169 			headerBuffer ~= data;
170 
171 			string statusLine;
172 			Headers headers;
173 
174 			debug(HTTP) auto oldData = headerBuffer.dup;
175 
176 			if (!parseHeaders(headerBuffer, statusLine, headers))
177 				return;
178 
179 			debug(HTTP)
180 			{
181 				stderr.writefln("Got response:");
182 				auto reqMessage = cast(string)oldData.bytes[0..oldData.bytes.length-headerBuffer.bytes.length].joinToHeap();
183 				foreach (line; reqMessage.split("\r\n"))
184 					stderr.writeln("< ", line);
185 			}
186 
187 			currentResponse = new HttpResponse;
188 			currentResponse.parseStatusLine(statusLine);
189 			currentResponse.headers = headers;
190 
191 			onHeadersReceived();
192 		}
193 		catch (CaughtException e)
194 		{
195 			if (conn.state == ConnectionState.connected)
196 				conn.disconnect(e.msg.length ? e.msg : e.classinfo.name, DisconnectType.error);
197 			else
198 				throw new Exception("Unhandled exception after connection was closed: " ~ e.msg, e);
199 		}
200 	}
201 
202 	/// Called when we've read all headers (currentResponse.headers is populated).
203 	void onHeadersReceived()
204 	{
205 		expect = size_t.max;
206 		// TODO: HEAD responses have Content-Length but no data!
207 		// We need to save a copy of the request (or at least the method) for that...
208 		if ("Content-Length" in currentResponse.headers)
209 			expect = currentResponse.headers["Content-Length"].strip().to!size_t();
210 
211 		conn.handleReadData = &onContinuation;
212 
213 		// Any remaining data in headerBuffer is now part of the response body
214 		// (and maybe even the headers of the next pipelined response).
215 		auto rest = move(headerBuffer);
216 		onData(rest[]);
217 	}
218 
219 	/// Received data handler used while we are receiving the response body.
220 	void onContinuation(Data data)
221 	{
222 		if (timer)
223 			timer.markNonIdle();
224 		onData(data.toArray);
225 	}
226 
227 	/// Called when we've received some data from the response body.
228 	void onData(scope Data[] data)
229 	{
230 		assert(!headerBuffer.length);
231 
232 		currentResponse.data ~= data;
233 
234 		auto received = currentResponse.data.bytes.length;
235 		if (expect != size_t.max && received >= expect)
236 		{
237 			// Any data past expect is part of the next response
238 			auto rest = currentResponse.data.bytes[expect .. received];
239 			currentResponse.data = currentResponse.data.bytes[0 .. expect];
240 			onDone(rest[], null, false);
241 		}
242 	}
243 
244 	/// Called when we've read the entirety of the response.
245 	/// Any left-over data is in `rest`.
246 	/// `disconnectReason` is `null` if there was no disconnect.
247 	void onDone(scope Data[] rest, string disconnectReason, bool error)
248 	{
249 		auto response = finalizeResponse();
250 		if (error)
251 			response = null; // Discard partial response
252 
253 		if (disconnectReason)
254 		{
255 			assert(rest is null);
256 		}
257 		else
258 		{
259 			if (keepAlive)
260 			{
261 				if (isIdle())
262 					onIdle();
263 				else
264 					expectResponse();
265 			}
266 			else
267 			{
268 				enforce(rest.bytes.length == 0, "Left-over data after non-keepalive response");
269 				conn.disconnect("All data read");
270 			}
271 		}
272 
273 		// This is done as the (almost) last step, so that we don't
274 		// have to worry about the user response handler changing our
275 		// state while we are in the middle of a function.
276 		submitResponse(response, disconnectReason);
277 
278 		// We still have to handle any left-over data as the last
279 		// step, because otherwise recursion will cause us to call the
280 		// handleResponse functions in the wrong order.
281 		if (rest.bytes.length)
282 			onHeaderData(rest);
283 	}
284 
285 	/// Wrap up and return the current response,
286 	/// and clean up the client for another request.
287 	HttpResponse finalizeResponse()
288 	{
289 		auto response = currentResponse;
290 		currentResponse = null;
291 		expect = -1;
292 
293 		if (!response || response.status != HttpStatusCode.Continue)
294 			receivedResponses++;
295 
296 		conn.handleReadData = null;
297 
298 		return response;
299 	}
300 
301 	/// Submit a received response.
302 	void submitResponse(HttpResponse response, string reason)
303 	{
304 		if (!reason)
305 			reason = "All data read";
306 		if (handleResponse)
307 			handleResponse(response, reason);
308 	}
309 
310 	/// Disconnect handler
311 	void onDisconnect(string reason, DisconnectType type)
312 	{
313 		// If we were expecting any more responses, we're not getting them.
314 		while (receivedResponses < sentRequests)
315 			onDone(null, reason, type == DisconnectType.error);
316 
317 		// If there are more requests queued (keepAlive == false),
318 		// reconnect and keep going.
319 		if (requestQueue.length)
320 			connect(requestQueue[0]);
321 	}
322 
323 	IConnection adaptConnection(IConnection conn)
324 	{
325 		return conn;
326 	}
327 
328 public:
329 	/// User-Agent header to advertise.
330 	string agent = "ae.net.http.client (+https://github.com/CyberShadow/ae)";
331 	/// Keep connection alive after one request.
332 	bool keepAlive = false;
333 	/// Send requests without waiting for a response. Requires keepAlive.
334 	bool pipelining = false;
335 
336 	/// Constructor.
337 	this(Duration timeout = 30.seconds, Connector connector = new TcpConnector)
338 	{
339 		assert(timeout >= Duration.zero);
340 
341 		this.connector = connector;
342 		IConnection c = connector.getConnection();
343 
344 		c = adaptConnection(c);
345 
346 		if (timeout > Duration.zero)
347 		{
348 			timer = new TimeoutAdapter(c);
349 			timer.setIdleTimeout(timeout);
350 			c = timer;
351 		}
352 
353 		conn = c;
354 		conn.handleConnect = &onConnect;
355 		conn.handleDisconnect = &onDisconnect;
356 	}
357 
358 	/// Fix up a response to set up required headers, etc.
359 	/// Done automatically by `request`, unless called with `normalize == false`.
360 	void normalizeRequest(HttpRequest request)
361 	{
362 		if ("User-Agent" !in request.headers && agent)
363 			request.headers["User-Agent"] = agent;
364 		if ("Accept-Encoding" !in request.headers)
365 			request.headers["Accept-Encoding"] = "gzip, deflate, identity;q=0.5, *;q=0";
366 		if (request.data)
367 			request.headers["Content-Length"] = to!string(request.data.bytes.length);
368 		if ("Connection" !in request.headers)
369 			request.headers["Connection"] = keepAlive ? "keep-alive" : "close";
370 	}
371 
372 	/// Send a HTTP request.
373 	void request(HttpRequest request, bool normalize = true)
374 	{
375 		if (normalize)
376 			normalizeRequest(request);
377 
378 		if (conn.state == ConnectionState.disconnected)
379 			connect(request);
380 		assert(conn.state <= ConnectionState.connected, "Attempting a HTTP request on a %s connection".format(conn.state));
381 
382 		requestQueue ~= request;
383 
384 		// |---------+------------+------------+---------------------------------------------------------------|
385 		// | enqueue | keep-alive | pipelining | outcome                                                       |
386 		// |---------+------------+------------+---------------------------------------------------------------|
387 		// | no      | no         | no         | one request and one connection at a time                      |
388 		// | no      | no         | yes        | error, need keep-alive for pipelining                         |
389 		// | no      | yes        | no         | keep connection alive so that we can send more requests later |
390 		// | no      | yes        | yes        | keep-alive + pipelining                                       |
391 		// | yes     | no         | no         | disconnect and connect again, once per queued request         |
392 		// | yes     | no         | yes        | error, need keep-alive for pipelining                         |
393 		// | yes     | yes        | no         | when one response is processed, send the next queued request  |
394 		// | yes     | yes        | yes        | send all requests at once after connecting                    |
395 		// |---------+------------+------------+---------------------------------------------------------------|
396 
397 		// |------------+------------+-----------------------------------------------------------------|
398 		// | keep-alive | pipelining | wat do in request()                                             |
399 		// |------------+------------+-----------------------------------------------------------------|
400 		// | no         | no         | assert(!connected), connect, enqueue                            |
401 		// | no         | yes        | assert                                                          |
402 		// | yes        | no         | enqueue or send now if connected; enqueue and connect otherwise |
403 		// | yes        | yes        | send now if connected; enqueue and connect otherwise            |
404 		// |------------+------------+-----------------------------------------------------------------|
405 
406 		if (!keepAlive)
407 		{
408 			if (!pipelining)
409 			{}
410 			else
411 				assert(false, "keepAlive is required for pipelining");
412 		}
413 		else
414 		{
415 			if (!pipelining)
416 			{
417 				// Can we send it now?
418 				if (isIdle())
419 					onIdle();
420 			}
421 			else
422 			{
423 				// Can we send it now?
424 				if (conn.state == ConnectionState.connected)
425 				{
426 					bool wasIdle = isIdle();
427 					assert(requestQueue.length == 1);
428 					while (requestQueue.length)
429 						sendRequest(requestQueue.shift);
430 					if (wasIdle)
431 						expectResponse();
432 				}
433 			}
434 		}
435 	}
436 
437 	/// Returns true if a connection is active
438 	/// (whether due to an in-flight request or due to keep-alive).
439 	bool connected()
440 	{
441 		if (receivedResponses < sentRequests)
442 			return true;
443 		if (keepAlive && conn.state == ConnectionState.connected)
444 			return true;
445 		return false;
446 	}
447 
448 	/// Close the connection to the HTTP server.
449 	void disconnect(string reason = IConnection.defaultDisconnectReason)
450 	{
451 		conn.disconnect(reason);
452 	}
453 
454 	/// User-supplied callback for handling the response.
455 	void delegate(HttpResponse response, string disconnectReason) handleResponse;
456 }
457 
458 /// HTTPS client.
459 class HttpsClient : HttpClient
460 {
461 	/// SSL context and adapter to use for TLS.
462 	SSLContext ctx;
463 	SSLAdapter adapter; /// ditto
464 
465 	/// Constructor.
466 	this(Duration timeout = 30.seconds)
467 	{
468 		ctx = ssl.createContext(SSLContext.Kind.client);
469 		super(timeout);
470 	}
471 
472 	protected override IConnection adaptConnection(IConnection conn)
473 	{
474 		adapter = ssl.createAdapter(ctx, conn);
475 		return adapter;
476 	}
477 
478 	protected override void connect(HttpRequest request)
479 	{
480 		super.connect(request);
481 		assert(conn.state == ConnectionState.connecting);
482 		adapter.setHostName(request.host);
483 	}
484 }
485 
486 // Experimental for now
487 class Connector
488 {
489 	abstract IConnection getConnection();
490 	abstract void connect(string host, ushort port);
491 }
492 
493 // ditto
494 class TcpConnector : Connector
495 {
496 	protected TcpConnection conn;
497 
498 	this()
499 	{
500 		conn = new TcpConnection();
501 	}
502 
503 	override IConnection getConnection()
504 	{
505 		return conn;
506 	}
507 
508 	override void connect(string host, ushort port)
509 	{
510 		conn.connect(host, port);
511 	}
512 }
513 
514 // ditto
515 version(Posix)
516 class UnixConnector : TcpConnector
517 {
518 	string path;
519 
520 	this(string path)
521 	{
522 		this.path = path;
523 	}
524 
525 	override void connect(string host, ushort port)
526 	{
527 		import std.socket;
528 		auto addr = new UnixAddress(path);
529 		conn.connect([AddressInfo(AddressFamily.UNIX, SocketType.STREAM, cast(ProtocolType)0, addr, path)]);
530 	}
531 }
532 
533 /// Asynchronous HTTP request
534 void httpRequest(HttpRequest request, void delegate(HttpResponse response, string disconnectReason) responseHandler)
535 {
536 	HttpClient client;
537 	if (request.protocol == "https")
538 		client = new HttpsClient;
539 	else
540 		client = new HttpClient;
541 
542 	client.handleResponse = responseHandler;
543 	client.request(request);
544 }
545 
546 /// ditto
547 void httpRequest(HttpRequest request, void delegate(Data) resultHandler, void delegate(string) errorHandler, int redirectCount = 0)
548 {
549 	void responseHandler(HttpResponse response, string disconnectReason)
550 	{
551 		if (!response)
552 			if (errorHandler)
553 				errorHandler(disconnectReason);
554 			else
555 				throw new Exception(disconnectReason);
556 		else
557 		if (response.status >= 300 && response.status < 400 && "Location" in response.headers)
558 		{
559 			if (redirectCount == 15)
560 				throw new Exception("HTTP redirect loop: " ~ request.url);
561 			request.resource = applyRelativeURL(request.url, response.headers["Location"]);
562 			if (response.status == HttpStatusCode.SeeOther)
563 			{
564 				request.method = "GET";
565 				request.data = null;
566 			}
567 			httpRequest(request, resultHandler, errorHandler, redirectCount+1);
568 		}
569 		else
570 			if (errorHandler)
571 				try
572 					resultHandler(response.getContent());
573 				catch (Exception e)
574 					errorHandler(e.msg);
575 			else
576 				resultHandler(response.getContent());
577 	}
578 
579 	httpRequest(request, &responseHandler);
580 }
581 
582 /// ditto
583 void httpGet(string url, void delegate(HttpResponse response, string disconnectReason) responseHandler)
584 {
585 	httpRequest(new HttpRequest(url), responseHandler);
586 }
587 
588 /// ditto
589 void httpGet(string url, void delegate(Data) resultHandler, void delegate(string) errorHandler)
590 {
591 	httpRequest(new HttpRequest(url), resultHandler, errorHandler);
592 }
593 
594 /// ditto
595 void httpGet(string url, void delegate(string) resultHandler, void delegate(string) errorHandler)
596 {
597 	httpGet(url,
598 		(Data data)
599 		{
600 			auto result = (cast(char[])data.contents).idup;
601 			std.utf.validate(result);
602 			resultHandler(result);
603 		},
604 		errorHandler);
605 }
606 
607 /// ditto
608 void httpPost(string url, DataVec postData, string contentType, void delegate(Data) resultHandler, void delegate(string) errorHandler)
609 {
610 	auto request = new HttpRequest;
611 	request.resource = url;
612 	request.method = "POST";
613 	if (contentType)
614 		request.headers["Content-Type"] = contentType;
615 	request.data = move(postData);
616 	httpRequest(request, resultHandler, errorHandler);
617 }
618 
619 /// ditto
620 void httpPost(string url, DataVec postData, string contentType, void delegate(string) resultHandler, void delegate(string) errorHandler)
621 {
622 	httpPost(url, move(postData), contentType,
623 		(Data data)
624 		{
625 			auto result = (cast(char[])data.contents).idup;
626 			std.utf.validate(result);
627 			resultHandler(result);
628 		},
629 		errorHandler);
630 }
631 
632 /// ditto
633 void httpPost(string url, UrlParameters vars, void delegate(string) resultHandler, void delegate(string) errorHandler)
634 {
635 	return httpPost(url, DataVec(Data(encodeUrlParameters(vars))), "application/x-www-form-urlencoded", resultHandler, errorHandler);
636 }
637 
638 // https://issues.dlang.org/show_bug.cgi?id=7016
639 version (unittest)
640 {
641 	static import ae.net.http.server;
642 	static import ae.net.http.responseex;
643 }
644 
645 unittest
646 {
647 	import ae.net.http.common : HttpRequest, HttpResponse;
648 	import ae.net.http.server : HttpServer, HttpServerConnection;
649 	import ae.net.http.responseex : HttpResponseEx;
650 
651 	foreach (enqueue; [false, true])
652 	foreach (keepAlive; [false, true])
653 	foreach (pipelining; [false, true])
654 	{
655 		if (pipelining && !keepAlive)
656 			continue;
657 		debug (HTTP) stderr.writefln("===== Testing enqueue=%s keepAlive=%s pipelining=%s", enqueue, keepAlive, pipelining);
658 
659 		auto s = new HttpServer;
660 		s.handleRequest = (HttpRequest _/*request*/, HttpServerConnection conn) {
661 			auto response = new HttpResponseEx;
662 			conn.sendResponse(response.serveText("Hello!"));
663 		};
664 		auto port = s.listen(0, "127.0.0.1");
665 
666 		auto c = new HttpClient;
667 		c.keepAlive = keepAlive;
668 		c.pipelining = pipelining;
669 		auto r = new HttpRequest("http://127.0.0.1:" ~ to!string(port));
670 		int count;
671 		c.handleResponse =
672 			(HttpResponse response, string _/*disconnectReason*/)
673 			{
674 				assert(response, "HTTP server error");
675 				assert(cast(string)response.getContent.toHeap == "Hello!");
676 				if (++count == 5)
677 				{
678 					s.close();
679 					if (keepAlive)
680 						c.disconnect();
681 				}
682 				else
683 					if (!enqueue)
684 						c.request(r);
685 			};
686 		foreach (n; 0 .. enqueue ? 5 : 1)
687 			c.request(r);
688 
689 		socketManager.loop();
690 
691 		assert(count == 5);
692 	}
693 }