1 /*
2  * Copyright (c) 2017-2018 sel-project
3  *
4  * Permission is hereby granted, free of charge, to any person obtaining a copy
5  * of this software and associated documentation files (the "Software"), to deal
6  * in the Software without restriction, including without limitation the rights
7  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8  * copies of the Software, and to permit persons to whom the Software is
9  * furnished to do so, subject to the following conditions:
10  *
11  * The above copyright notice and this permission notice shall be included in all
12  * copies or substantial portions of the Software.
13  *
14  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
20  * SOFTWARE.
21  *
22  */
23 /**
24  * Copyright: Copyright (c) 2017-2018 sel-project
25  * License: MIT
26  * Authors: Kripth
27  * Source: $(HTTP github.com/sel-project/sel-net/sel/net/http.d, sel/net/http.d)
28  */
29 module sel.net.http;
30 
31 import std.array : Appender;
32 import std.conv : to, ConvException;
33 import std.socket : Socket;
34 import std.string : join, split, toUpper, toLower, strip;
35 import std.traits : EnumMembers;
36 
37 import sel.net.stream : Stream, TcpStream;
38 
39 /**
40  * Stream container for HTTP connection that reads/writes strings
41  * instead of array of bytes.
42  */
43 class HttpStream {
44 
45 	private Stream stream;
46 
47 	public this(Stream stream) {
48 		this.stream = stream;
49 	}
50 
51 	public this(Socket socket) {
52 		this(new TcpStream(socket));
53 	}
54 
55 	public ptrdiff_t send(string payload) {
56 		return this.stream.send(cast(ubyte[])payload);
57 	}
58 
59 	public string receive() {
60 		return cast(string)this.stream.receive();
61 	}
62 
63 }
64 
65 /**
66  * Indicates the status of an HTTP response.
67  */
68 struct Status {
69 
70 	/**
71 	 * HTTP response status code.
72 	 */
73 	uint code;
74 
75 	/**
76 	 * Additional short description of the status code.
77 	 */
78 	string message;
79 
80 	bool opEquals(uint code) {
81 		return this.code == code;
82 	}
83 
84 	bool opEquals(Status status) {
85 		return this.opEquals(status.code);
86 	}
87 
88 	/**
89 	 * Concatenates the status code and the message into
90 	 * a string.
91 	 * Example:
92 	 * ---
93 	 * assert(Status(200, "OK").toString() == "200 OK");
94 	 * ---
95 	 */
96 	string toString() {
97 		return this.code.to!string ~ " " ~ this.message;
98 	}
99 
100 	/**
101 	 * Creates a status from a known list of codes/messages.
102 	 * Example:
103 	 * ---
104 	 * assert(Status.get(200).message == "OK");
105 	 * ---
106 	 */
107 	public static Status get(uint code) {
108 		foreach(statusCode ; [EnumMembers!StatusCodes]) {
109 			if(code == statusCode.code) return statusCode;
110 		}
111 		return Status(code, "Unknown Status Code");
112 	}
113 
114 }
115 
116 /**
117  * HTTP status codes and their human-readable names.
118  */
119 enum StatusCodes : Status {
120 
121 	// informational
122 	continue_ = Status(100, "Continue"),
123 	switchingProtocols = Status(101, "Switching Protocols"),
124 
125 	// success
126 	ok = Status(200, "OK"),
127 	created = Status(201, "Created"),
128 	accepted = Status(202, "Accepted"),
129 	nonAuthoritativeContent = Status(203, "Non-Authoritative Information"),
130 	noContent = Status(204, "No Content"),
131 	resetContent = Status(205, "Reset Content"),
132 	partialContent = Status(206, "Partial Content"),
133 
134 	// redirection
135 	multipleChoices = Status(300, "Multiple Choices"),
136 	movedPermanently = Status(301, "Moved Permanently"),
137 	found = Status(302, "Found"),
138 	seeOther = Status(303, "See Other"),
139 	notModified = Status(304, "Not Modified"),
140 	useProxy = Status(305, "Use Proxy"),
141 	switchProxy = Status(306, "Switch Proxy"),
142 	temporaryRedirect = Status(307, "Temporary Redirect"),
143 	permanentRedirect = Status(308, "Permanent Redirect"),
144 
145 	// client errors
146 	badRequest = Status(400, "Bad Request"),
147 	unauthorized = Status(401, "Unauthorized"),
148 	paymentRequired = Status(402, "Payment Required"),
149 	forbidden = Status(403, "Forbidden"),
150 	notFound = Status(404, "Not Found"),
151 	methodNotAllowed = Status(405, "Method Not Allowed"),
152 	notAcceptable = Status(406, "Not Acceptable"),
153 	proxyAuthenticationRequired = Status(407, "Proxy Authentication Required"),
154 	requestTimeout = Status(408, "Request Timeout"),
155 	conflict = Status(409, "Conflict"),
156 	gone = Status(410, "Gone"),
157 	lengthRequired = Status(411, "Length Required"),
158 	preconditionFailed = Status(412, "Precondition Failed"),
159 	payloadTooLarge = Status(413, "Payload Too Large"),
160 	uriTooLong = Status(414, "URI Too Long"),
161 	unsupportedMediaType = Status(415, "UnsupportedMediaType"),
162 	rangeNotSatisfiable = Status(416, "Range Not Satisfiable"),
163 	expectationFailed = Status(417, "Expectation Failed"),
164 
165 	// server errors
166 	internalServerError = Status(500, "Internal Server Error"),
167 	notImplemented = Status(501, "Not Implemented"),
168 	badGateway = Status(502, "Bad Gateway"),
169 	serviceUnavailable = Status(503, "Service Unavailable"),
170 	gatewayTimeout = Status(504, "Gateway Timeout"),
171 	httpVersionNotSupported = Status(505, "HTTP Version Not Supported"),
172 
173 }
174 
175 private enum defaultHeaders = ["Server": "sel-net/1.0"]; //TODO load version from .dub/version.json
176 
177 /**
178  * Container for a HTTP request.
179  * Example:
180  * ---
181  * Request("GET", "/");
182  * Request(Request.POST, "/subscribe.php");
183  * ---
184  */
185 struct Request {
186 
187 	enum GET = "GET";
188 	enum POST = "POST";
189 
190 	/**
191 	 * Method used in the request (i.e. GET). Must be uppercase.
192 	 */
193 	string method;
194 
195 	/**
196 	 * Path of the request. It should start with a slash.
197 	 */
198 	string path;
199 
200 	/**
201 	 * HTTP headers of the request.
202 	 */
203 	string[string] headers;
204 
205 	/**
206 	 * Optional raw form data, for POST requests.
207 	 */
208 	string data;
209 
210 	/**
211 	 * If the request was parsed, indicates whether it was in a
212 	 * valid HTTP format.
213 	 */
214 	bool valid;
215 
216 	public this(string method, string path, string[string] headers=defaultHeaders) {
217 		this.method = method;
218 		this.path = path;
219 		this.headers = headers;
220 	}
221 
222 	/**
223 	 * Creates a get request.
224 	 * Example:
225 	 * ---
226 	 * auto get = Request.get("/index.html", ["Host": "127.0.0.1"]);
227 	 * assert(get.toString() == "GET /index.html HTTP/1.1\r\nHost: 127.0.0.1\r\n");
228 	 * ---
229 	 */
230 	public static Request get(string path, string[string] headers=defaultHeaders) {
231 		return Request(GET, path, headers);
232 	}
233 
234 	/**
235 	 * Creates a post request.
236 	 * Example:
237 	 * ---
238 	 * auto post = Request.post("/sub.php", ["Connection": "Keep-Alive"], "name=Mark&surname=White");
239 	 * assert(post.toString() == "POST /sub.php HTTP/1.1\r\nConnection: Keep-Alive\r\n\r\nname=Mark&surname=White");
240 	 * ---
241 	 */
242 	public static Request post(string path, string[string] headers=defaultHeaders, string data="") {
243 		Request request = Request(POST, path, headers);
244 		request.data = data;
245 		return request;
246 	}
247 
248 	/// ditto
249 	public static Request post(string path, string data, string[string] headers=defaultHeaders) {
250 		return post(path, headers, data);
251 	}
252 
253 	/**
254 	 * Encodes the request into a string.
255 	 * Example:
256 	 * ---
257 	 * auto request = Request(Request.GET, "index.html", ["Connection": "Keep-Alive"]);
258 	 * assert(request.toString() == "GET /index.html HTTP/1.1\r\nConnection: Keep-Alive\r\n");
259 	 * ---
260 	 */
261 	public string toString() {
262 		if(this.data.length) this.headers["Content-Length"] = to!string(this.data.length);
263 		return encodeHTTP(this.method.toUpper() ~ " " ~ this.path ~ " HTTP/1.1", this.headers, this.data);
264 	}
265 
266 	/**
267 	 * Parses a string and returns a Request.
268 	 * If the request is successfully parsed Request.valid will be true.
269 	 * Please note that every key in the header is converted to lowercase for
270 	 * an easier search in the associative array.
271 	 * Example:
272 	 * ---
273 	 * auto request = Request.parse("GET /index.html HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: Keep-Alive\r\n");
274 	 * assert(request.valid);
275 	 * assert(request.method == Request.GET);
276 	 * assert(request.headers["Host"] == "127.0.0.1");
277 	 * assert(request.headers["Connection"] == "Keep-Alive");
278 	 * ---
279 	 */
280 	public static Request parse(string str) {
281 		Request request;
282 		string status;
283 		if(decodeHTTP(str, status, request.headers, request.data)) {
284 			string[] spl = status.split(" ");
285 			if(spl.length == 3) {
286 				request.valid = true;
287 				request.method = spl[0];
288 				request.path = spl[1];
289 			}
290 		}
291 		return request;
292 	}
293 	
294 }
295 
296 /**
297  * Container for an HTTP response.
298  * Example:
299  * ---
300  * Response(200, ["Connection": "Close"], "<b>Hi there</b>");
301  * Response(404, [], "Cannot find the specified path");
302  * Response(204);
303  * ---
304  */
305 struct Response {
306 
307 	/**
308 	 * Status of the response.
309 	 */
310 	Status status;
311 
312 	/**
313 	 * HTTP headers of the request.
314 	 */
315 	string[string] headers;
316 
317 	/**
318 	 * Content of the request. Its type should be specified in
319 	 * the `content-type` field in the headers.
320 	 */
321 	string content;
322 
323 	/**
324 	 * If the response was parsed, indicates whether it was in a
325 	 * valid HTTP format.
326 	 */
327 	bool valid;
328 	
329 	public this(Status status, string[string] headers=defaultHeaders, string content="") {
330 		this.status = status;
331 		this.headers = headers;
332 		this.content = content;
333 	}
334 		
335 	public this(uint statusCode, string[string] headers=defaultHeaders, string content="") {
336 		this(Status.get(statusCode), headers, content);
337 	}
338 
339 	public this(Status status, string content) {
340 		this(status, defaultHeaders, content);
341 	}
342 
343 	public this(uint statusCode, string content) {
344 		this(statusCode, defaultHeaders, content);
345 	}
346 
347 	/**
348 	 * Creates a response for an HTTP error an automatically generates
349 	 * an HTML page to display it.
350 	 * Example:
351 	 * ---
352 	 * Response.error(404);
353 	 * Response.error(StatusCodes.methodNotAllowed, ["Allow": "GET"]);
354 	 * ---
355 	 */
356 	public static Response error(Status status, string[string] headers=defaultHeaders) {
357 		immutable message = status.toString();
358 		headers["Content-Type"] = "text/html";
359 		return Response(status, headers, "<!DOCTYPE html><html><head><title>" ~ message ~ "</title></head><body><center><h1>" ~ message ~ "</h1></center><hr><center>" ~ headers.get("Server", "sel-net") ~ "</center></body></html>");
360 	}
361 
362 	/// ditto
363 	public static Response error(uint statusCode, string[string] headers=defaultHeaders) {
364 		return error(Status.get(statusCode), headers);
365 	}
366 
367 	/**
368 	 * Creates a 3xx redirect response and adds the `Location` field to
369 	 * the header.
370 	 * If not specified status code `301 Moved Permanently` will be used.
371 	 * Example:
372 	 * ---
373 	 * Response.redirect("/index.html");
374 	 * Response.redirect(302, "/view.php");
375 	 * Response.redirect(StatusCodes.seeOther, "/icon.png", ["Server": "sel-net"]);
376 	 * ---
377 	 */
378 	public static Response redirect(Status status, string location, string[string] headers=defaultHeaders) {
379 		headers["Location"] = location;
380 		return Response(status, headers);
381 	}
382 
383 	/// ditto
384 	public static Response redirect(uint statusCode, string location, string[string] headers=defaultHeaders) {
385 		return redirect(Status.get(statusCode), location, headers);
386 	}
387 
388 	/// ditto
389 	public static Response redirect(string location, string[string] headers=defaultHeaders) {
390 		return redirect(StatusCodes.movedPermanently, location, headers);
391 	}
392 
393 	/**
394 	 * Encodes the response into a string.
395 	 * The `Content-Length` header field is created automatically
396 	 * based on the length of the content field.
397 	 * Example:
398 	 * ---
399 	 * auto response = Response(200, [], "Hi");
400 	 * assert(response.toString() == "HTTP/1.1 200 OK\r\nContent-Length: 2\r\n\r\nHi");
401 	 * ---
402 	 */
403 	public string toString() {
404 		this.headers["Content-Length"] = to!string(this.content.length);
405 		return encodeHTTP("HTTP/1.1 " ~ this.status.toString(), this.headers, this.content);
406 	}
407 
408 	/**
409 	 * Parses a string and returns a Response.
410 	 * If the response is successfully parsed Response.valid will be true.
411 	 * Please note that every key in the header is converted to lowercase for
412 	 * an easier search in the associative array.
413 	 * Example:
414 	 * ---
415 	 * auto response = Response.parse("HTTP/1.1 200 OK\r\nContent-Type: plain/text\r\nContent-Length: 4\r\n\r\ntest");
416 	 * assert(response.valid);
417 	 * assert(response.status == 200);
418 	 * assert(response.headers["content-type"] == "text/plain");
419 	 * assert(response.headers["content-length"] == "4");
420 	 * assert(response.content == "test");
421 	 * ---
422 	 */
423 	public static Response parse(string str) {
424 		Response response;
425 		string status;
426 		if(decodeHTTP(str, status, response.headers, response.content)) {
427 			string[] head = status.split(" ");
428 			if(head.length >= 3) {
429 				try {
430 					response.status = Status(to!uint(head[1]), join(head[2..$], " "));
431 					response.valid = true;
432 				} catch(ConvException) {}
433 			}
434 		}
435 		return response;
436 	}
437 	
438 }
439 
440 private enum CR_LF = "\r\n";
441 
442 private string encodeHTTP(string status, string[string] headers, string content) {
443 	Appender!string ret;
444 	ret.put(status);
445 	ret.put(CR_LF);
446 	foreach(key, value; headers) {
447 		ret.put(key);
448 		ret.put(": ");
449 		ret.put(value);
450 		ret.put(CR_LF);
451 	}
452 	ret.put(CR_LF); // empty line
453 	ret.put(content);
454 	return ret.data;
455 }
456 
457 private bool decodeHTTP(string str, ref string status, ref string[string] headers, ref string content) {
458 	string[] spl = str.split(CR_LF);
459 	if(spl.length > 1) {
460 		status = spl[0];
461 		size_t index;
462 		while(++index < spl.length && spl[index].length) { // read until empty line
463 			auto s = spl[index].split(":");
464 			if(s.length >= 2) {
465 				headers[s[0].strip.toLower()] = s[1..$].join(":").strip;
466 			} else {
467 				return false; // invalid header
468 			}
469 		}
470 		content = join(spl[index+1..$], "\r\n");
471 		return true;
472 	} else {
473 		return false;
474 	}
475 }