1 module lighttp.util;
2 
3 import std.array : Appender;
4 import std.conv : to, ConvException;
5 import std.datetime : DateTime;
6 import std.json : JSONValue;
7 import std.regex : ctRegex;
8 import std.string : toUpper, toLower, split, join, strip, indexOf;
9 import std.traits : EnumMembers;
10 import std.uri : encode, decodeComponent;
11 
12 import libasync : NetworkAddress;
13 
14 import url : URL;
15 
16 /**
17  * Indicates the status of an HTTP response.
18  */
19 struct Status {
20 	
21 	/**
22 	 * HTTP response status code.
23 	 */
24 	uint code = 200;
25 	
26 	/**
27 	 * Additional short description of the status code.
28 	 */
29 	string message;
30 	
31 	bool opEquals(uint code) {
32 		return this.code == code;
33 	}
34 	
35 	bool opEquals(Status status) {
36 		return this.opEquals(status.code);
37 	}
38 	
39 	/**
40 	 * Concatenates the status code and the message into
41 	 * a string.
42 	 * Example:
43 	 * ---
44 	 * assert(Status(200, "OK").toString() == "200 OK");
45 	 * ---
46 	 */
47 	string toString() {
48 		return this.code.to!string ~ " " ~ this.message;
49 	}
50 	
51 	/**
52 	 * Creates a status from a known list of codes/messages.
53 	 * Example:
54 	 * ---
55 	 * assert(Status.get(200).message == "OK");
56 	 * ---
57 	 */
58 	public static Status get(uint code) {
59 		foreach(statusCode ; [EnumMembers!StatusCodes]) {
60 			if(code == statusCode.code) return statusCode;
61 		}
62 		return Status(code, "Unknown Status Code");
63 	}
64 	
65 }
66 
67 /**
68  * HTTP status codes and their human-readable names.
69  */
70 enum StatusCodes : Status {
71 	
72 	// informational
73 	continue_ = Status(100, "Continue"),
74 	switchingProtocols = Status(101, "Switching Protocols"),
75 	
76 	// successful
77 	ok = Status(200, "OK"),
78 	created = Status(201, "Created"),
79 	accepted = Status(202, "Accepted"),
80 	nonAuthoritativeContent = Status(203, "Non-Authoritative Information"),
81 	noContent = Status(204, "No Content"),
82 	resetContent = Status(205, "Reset Content"),
83 	partialContent = Status(206, "Partial Content"),
84 	
85 	// redirection
86 	multipleChoices = Status(300, "Multiple Choices"),
87 	movedPermanently = Status(301, "Moved Permanently"),
88 	found = Status(302, "Found"),
89 	seeOther = Status(303, "See Other"),
90 	notModified = Status(304, "Not Modified"),
91 	useProxy = Status(305, "Use Proxy"),
92 	switchProxy = Status(306, "Switch Proxy"),
93 	temporaryRedirect = Status(307, "Temporary Redirect"),
94 	permanentRedirect = Status(308, "Permanent Redirect"),
95 	
96 	// client error
97 	badRequest = Status(400, "Bad Request"),
98 	unauthorized = Status(401, "Unauthorized"),
99 	paymentRequired = Status(402, "Payment Required"),
100 	forbidden = Status(403, "Forbidden"),
101 	notFound = Status(404, "Not Found"),
102 	methodNotAllowed = Status(405, "Method Not Allowed"),
103 	notAcceptable = Status(406, "Not Acceptable"),
104 	proxyAuthenticationRequired = Status(407, "Proxy Authentication Required"),
105 	requestTimeout = Status(408, "Request Timeout"),
106 	conflict = Status(409, "Conflict"),
107 	gone = Status(410, "Gone"),
108 	lengthRequired = Status(411, "Length Required"),
109 	preconditionFailed = Status(412, "Precondition Failed"),
110 	payloadTooLarge = Status(413, "Payload Too Large"),
111 	uriTooLong = Status(414, "URI Too Long"),
112 	unsupportedMediaType = Status(415, "Unsupported Media Type"),
113 	rangeNotSatisfiable = Status(416, "Range Not Satisfiable"),
114 	expectationFailed = Status(417, "Expectation Failed"),
115 	
116 	// server error
117 	internalServerError = Status(500, "Internal Server Error"),
118 	notImplemented = Status(501, "Not Implemented"),
119 	badGateway = Status(502, "Bad Gateway"),
120 	serviceUnavailable = Status(503, "Service Unavailable"),
121 	gatewayTimeout = Status(504, "Gateway Timeout"),
122 	httpVersionNotSupported = Status(505, "HTTP Version Not Supported"),
123 	
124 }
125 
126 /**
127  * Frequently used mime types.
128  */
129 enum MimeTypes : string {
130 	
131 	// text
132 	html = "text/html",
133 	javascript = "text/javascript",
134 	css = "text/css",
135 	text = "text/plain",
136 	
137 	// images
138 	png = "image/png",
139 	jpeg = "image/jpeg",
140 	gif = "image/gif",
141 	ico = "image/x-icon",
142 	svg = "image/svg+xml",
143 	
144 	// other
145 	json = "application/json",
146 	zip = "application/zip",
147 	bin = "application/octet-stream",
148 	
149 }
150 
151 struct Cookie {
152 
153 	string name;
154 	string value;
155 	DateTime expires;
156 	size_t maxAge;
157 	string domain;
158 	string path;
159 	bool secure;
160 	bool httpOnly;
161 
162 	string toString() {
163 		Appender!string ret;
164 		ret.put(name);
165 		ret.put("=");
166 		ret.put(value);
167 		if(expires != DateTime.init) {
168 			ret.put(";Expires=");
169 			ret.put(expires.toString()); //TODO format correctly
170 		}
171 		if(maxAge != size_t.init) {
172 			ret.put(";Max-Age=");
173 			ret.put(maxAge.to!string);
174 		}
175 		if(domain.length) {
176 			ret.put(";Domain=");
177 			ret.put(domain);
178 		}
179 		if(path.length) {
180 			ret.put(";Path=");
181 			ret.put(path);
182 		}
183 		if(secure) ret.put(";Secure");
184 		if(httpOnly) ret.put(";HttpOnly");
185 		return ret.data;
186 	}
187 
188 }
189 
190 private enum User {
191 
192 	client,
193 	server
194 
195 }
196 
197 private enum Type {
198 
199 	request,
200 	response
201 
202 }
203 
204 /**
205  * Base class for requests and responses.
206  */
207 abstract class Http {
208 
209 	/**
210 	 * Headers utility.
211 	 */
212 	static struct Headers {
213 		
214 		static struct Header {
215 			
216 			string key;
217 			string value;
218 			
219 		}
220 		
221 		private Header[] _headers;
222 		size_t[string] _headersIndexes;
223 
224 		/**
225 		 * Gets the pointer to a header.
226 		 * The key is case insensitive.
227 		 * Example:
228 		 * ---
229 		 * headers["Connection"] = "keep-alive";
230 		 * assert(("connection" in headers) is ("Connection" in headers));
231 		 * ---
232 		 */
233 		string* opBinaryRight(string op : "in")(string key) pure @safe {
234 			if(auto ptr = key.toLower in _headersIndexes) return &_headers[*ptr].value;
235 			else return null;
236 		}
237 
238 		/**
239 		 * Gets the value of a header.
240 		 * The key is case insensitive.
241 		 * Example:
242 		 * ---
243 		 * headers["Connection"] = "keep-alive";
244 		 * assert(header["connection"] == "keep-alive");
245 		 * ---
246 		 */
247 		string opIndex(string key) pure @safe {
248 			return _headers[_headersIndexes[key.toLower]].value;
249 		}
250 		
251 		/**
252 		 * Gets the value of a header and returns `defaultValue` if
253 		 * it does not exist.
254 		 * Example:
255 		 * ---
256 		 * assert(headers.get("Connection", "close") == "close");
257 		 * headers["Connection"] = "keep-alive";
258 		 * assert(headers.get("Connection", "close") != "close");
259 		 * ---
260 		 */
261 		string get(string key, lazy string defaultValue) pure @safe {
262 			if(auto ptr = key.toLower in _headersIndexes) return _headers[*ptr].value;
263 			else return defaultValue;
264 		}
265 
266 		/**
267 		 * Assigns, and overrides if it already exists, a header value.
268 		 * The key is case-insensitive.
269 		 * Example:
270 		 * ---
271 		 * headers["Connection"] = "keep-alive";
272 		 * headers["connection"] = "close";
273 		 * assert(headers["Connection"] == "close");
274 		 * --
275 		 */
276 		string opIndexAssign(string value, string key) pure @safe {
277 			if(auto ptr = key.toLower in _headersIndexes) return _headers[*ptr].value = value;
278 			else {
279 				_headersIndexes[key.toLower] = _headers.length;
280 				_headers ~= Header(key, value);
281 				return value;
282 			}
283 		}
284 
285 		/**
286 		 * Adds a new key-value pair to the headers without overriding
287 		 * the existing ones.
288 		 * Example:
289 		 * ---
290 		 * headers.add("Set-Cookie", "a=b");
291 		 * headers.add("Set-Cookie", "b=c");
292 		 * ---
293 		 */
294 		void add(string key, string value) pure @safe {
295 			if(!(key.toLower in _headersIndexes)) _headersIndexes[key.toLower] = _headers.length;
296 			_headers ~= Header(key, value);
297 		}
298 		
299 		@property Header[] headers() {
300 			return _headers;
301 		}
302 		
303 	}
304 
305 	protected string _method;
306 
307 	/**
308 	 * Gets and sets the status of the request/response.
309 	 * The value can be one of the enum `StatusCodes` or a custom
310 	 * status using the `Status` struct.
311 	 */
312 	public Status status;
313 
314 	/**
315 	 * Gets the header manager of the request/response.
316 	 * The available methods are the same of the associative array's
317 	 * except that keys are case-insensitive.
318 	 * Example:
319 	 * ---
320 	 * headers["Connection"] = "keep-alive";
321 	 * assert(headers["connection"] == "keep-alive");
322 	 * ---
323 	 */
324 	public Headers headers;
325 
326 	private bool _cookiesInit = false;
327 	private string[string] _cookies;
328 
329 	protected string _body;
330 
331 	/**
332 	 * Gets the method used for the request.
333 	 */
334 	public @property string method() pure nothrow @safe @nogc {
335 		return _method;
336 	}
337 
338 	/**
339 	 * Gets the cookies sent in the `Cookie` header in the request.
340 	 * This property is lazily initialized.
341 	 */
342 	public @property string[string] cookies() {
343 		if(!_cookiesInit) {
344 			if(auto cookies = "cookie" in headers) {
345 				foreach(cookie ; split(*cookies, ";")) {
346 					cookie = cookie.strip;
347 					immutable eq = cookie.indexOf("=");
348 					if(eq > 0) {
349 						_cookies[cookie[0..eq]] = cookie[eq+1..$];
350 					}
351 				}
352 			}
353 			_cookiesInit = true;
354 		}
355 		return _cookies;
356 	}
357 
358 	/**
359 	 * Adds a cookie to the response's header.
360 	 */
361 	public void add(Cookie cookie) {
362 		this.headers.add("Set-Cookie", cookie.toString());
363 	}
364 	
365 	/**
366 	 * Gets the body of the request/response.
367 	 */
368 	public @property string body_() pure nothrow @safe @nogc {
369 		return _body;
370 	}
371 	
372 	/**
373 	 * Sets the body of the request/response.
374 	 */
375 	public @property string body_(T)(T data) {
376 		static if(is(T : string)) {
377 			return _body = cast(string)data;
378 		} else static if(is(T == JSONValue)) {
379 			this.contentType = MimeTypes.json;
380 			return _body = data.toString();
381 		} else static if(is(T == JSONValue[string]) || is(T == JSONValue[])) {
382 			return body_ = JSONValue(data);
383 		} else {
384 			return _body = data.to!string;
385 		}
386 	}
387 	
388 	/// ditto
389 	static if(__VERSION__ >= 2078) alias body = body_;
390 	
391 	/**
392 	 * Sets the response's content-type header.
393 	 * Example:
394 	 * ---
395 	 * response.contentType = MimeTypes.html;
396 	 * ---
397 	 */
398 	public @property string contentType(string contentType) pure @safe {
399 		return this.headers["Content-Type"] = contentType;
400 	}
401 
402 	public abstract bool parse(string);
403 
404 }
405 
406 /**
407  * Class for request and response.
408  */
409 template HttpImpl(User user, Type type) {
410 
411 	class HttpImpl : Http {
412 
413 		static if(user == User.client && type == Type.response || user == User.server && type == Type.request) public NetworkAddress address;
414 
415 		static if(type == Type.request) private URL _url;
416 
417 		static if(user == User.server && type == Type.response) {
418 
419 			public void delegate() send;
420 			public bool ready = true;
421 
422 		}
423 
424 		static if(user == user.client && type == Type.request) public this(string method, string path, string body_="") {
425 			_method = method;
426 			_url.path = path;
427 			_body = body_;
428 		}
429 
430 		/**
431 		 * Gets the url of the request. `url.path` can be used to
432 		 * retrive the path and `url.queryParams` to retrive the query
433 		 * parameters.
434 		 */
435 		static if(type == Type.request) public @property URL url() pure nothrow @safe @nogc {
436 			return _url;
437 		}
438 
439 		static if(user == User.server && type == Type.response) {
440 		
441 			/**
442 			 * Creates a 3xx redirect response and adds the `Location` field to
443 			 * the header.
444 			 * If not specified status code `301 Moved Permanently` will be used.
445 			 * Example:
446 			 * ---
447 			 * http.redirect("/index.html");
448 			 * http.redirect(302, "/view.php");
449 			 * http.redirect(StatusCodes.seeOther, "/icon.png");
450 			 * ---
451 			 */
452 			public void redirect(Status status, string location) {
453 				this.status = status;
454 				this.headers["Location"] = location;
455 				this.headers["Connection"] = "keep-alive";
456 			}
457 			
458 			/// ditto
459 			public void redirect(uint statusCode, string location) {
460 				this.redirect(Status.get(statusCode), location);
461 			}
462 			
463 			/// ditto
464 			public void redirect(string location) {
465 				this.redirect(StatusCodes.movedPermanently, location);
466 			}
467 
468 		}
469 
470 		public override string toString() {
471 			this.headers["Content-Length"] = to!string(this.body_.length);
472 			static if(type == Type.request) {
473 				return encodeHTTP(this.method.toUpper() ~ " " ~ encode(this.url.path ~ this.url.queryParams.toString()) ~ " HTTP/1.1", this.headers.headers, this.body_);
474 			} else {
475 				return encodeHTTP("HTTP/1.1 " ~ this.status.toString(), this.headers.headers, this.body_);
476 			}
477 		}
478 
479 		public override bool parse(string data) {
480 			string status;
481 			static if(type == Type.request) {
482 				if(decodeHTTP(data, status, this.headers, this._body)) {
483 					string[] spl = status.split(" ");
484 					if(spl.length == 3) {
485 						_method = spl[0];
486 						immutable q = spl[1].indexOf("?");
487 						if(q >= 0) {
488 							_url.path = decodeComponent(spl[1][0..q]);
489 							foreach(param ; spl[1][q+1..$].split("&")) {
490 								immutable eq = param.indexOf("=");
491 								if(eq >= 0) {
492 									_url.queryParams.add(decodeComponent(param[0..eq]), decodeComponent(param[eq+1..$]));
493 								} else {
494 									_url.queryParams.add(decodeComponent(param), "");
495 								}
496 							}
497 						} else {
498 							_url.path = decodeComponent(spl[1]);
499 						}
500 						return true;
501 					}
502 				}
503 			} else {
504 				if(decodeHTTP(data, status, this.headers, this._body)) {
505 					string[] head = status.split(" ");
506 					if(head.length >= 3) {
507 						try {
508 							this.status = Status(to!uint(head[1]), join(head[2..$], " "));
509 							return true;
510 						} catch(ConvException) {}
511 					}
512 				}
513 			}
514 			return false;
515 		}
516 
517 	}
518 
519 }
520 
521 alias ClientRequest = HttpImpl!(User.client, Type.request);
522 
523 alias ClientResponse = HttpImpl!(User.client, Type.response);
524 
525 alias ServerRequest = HttpImpl!(User.server, Type.request);
526 
527 alias ServerResponse = HttpImpl!(User.server, Type.response);
528 
529 private enum crlf = "\r\n";
530 
531 private string encodeHTTP(string status, Http.Headers.Header[] headers, string content) {
532 	Appender!string ret;
533 	ret.put(status);
534 	ret.put(crlf);
535 	foreach(header ; headers) {
536 		ret.put(header.key);
537 		ret.put(": ");
538 		ret.put(header.value);
539 		ret.put(crlf);
540 	}
541 	ret.put(crlf); // empty line
542 	ret.put(content);
543 	return ret.data;
544 }
545 
546 private bool decodeHTTP(string str, ref string status, ref Http.Headers headers, ref string content) {
547 	string[] spl = str.split(crlf);
548 	if(spl.length > 1) {
549 		status = spl[0];
550 		size_t index;
551 		while(++index < spl.length && spl[index].length) { // read until empty line
552 			auto s = spl[index].split(":");
553 			if(s.length >= 2) {
554 				headers.add(s[0].strip, s[1..$].join(":").strip);
555 			} else {
556 				return false; // invalid header
557 			}
558 		}
559 		content = (index + 1 < spl.length) ? join(spl[index + 1..$], crlf) : string.init;
560 		return true;
561 	} else {
562 		return false;
563 	}
564 }