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 }