1 /**
2  * An improved HttpResponse class to ease writing pages.
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  *   Vladimir Panteleev <ae@cy.md>
12  *   Simon Arlott
13  */
14 
15 module ae.net.http.responseex;
16 
17 import std.algorithm.searching : skipOver, findSplit;
18 import std.base64;
19 import std.exception;
20 import std.string;
21 import std.conv;
22 import std.file;
23 import std.path;
24 
25 public import ae.net.http.common;
26 import ae.net.ietf.headers;
27 import ae.sys.data;
28 import ae.sys.dataio;
29 import ae.utils.array;
30 import ae.utils.json;
31 import ae.utils.xml;
32 import ae.utils.text;
33 import ae.utils.mime;
34 
35 /// HttpResponse with some code to ease creating responses
36 final class HttpResponseEx : HttpResponse
37 {
38 public:
39 	/// Redirect the UA to another location
40 	HttpResponseEx redirect(string location, HttpStatusCode status = HttpStatusCode.SeeOther)
41 	{
42 		setStatus(status);
43 		headers["Location"] = location;
44 		return this;
45 	}
46 
47 	/// Utility function to serve HTML.
48 	HttpResponseEx serveData(string data, string contentType = "text/html; charset=utf-8")
49 	{
50 		return serveData(Data(data), contentType);
51 	}
52 
53 	/// Utility function to serve arbitrary data.
54 	HttpResponseEx serveData(Data[] data, string contentType)
55 	{
56 		setStatus(HttpStatusCode.OK);
57 		headers["Content-Type"] = contentType;
58 		this.data = data;
59 		return this;
60 	}
61 
62 	/// ditto
63 	HttpResponseEx serveData(Data data, string contentType)
64 	{
65 		return serveData([data], contentType);
66 	}
67 
68 	/// If set, this is the name of the JSONP callback function to be
69 	/// used in `serveJson`.
70 	string jsonCallback;
71 
72 	/// Utility function to serialize and serve an arbitrary D value as JSON.
73 	/// If `jsonCallback` is set, use JSONP instead.
74 	HttpResponseEx serveJson(T)(T v)
75 	{
76 		string data = toJson(v);
77 		if (jsonCallback)
78 			return serveData(jsonCallback~'('~data~')', "text/javascript");
79 		else
80 			return serveData(data, "application/json");
81 	}
82 
83 	/// Utility function to serve plain text.
84 	HttpResponseEx serveText(string data)
85 	{
86 		return serveData(Data(data), "text/plain; charset=utf-8");
87 	}
88 
89 	private static bool checkPath(string path)
90 	{
91 		if (!path.length)
92 			return true;
93 		if (path.contains("..") || path[0]=='/' || path[0]=='\\' || path.contains("//") || path.contains("\\\\"))
94 			return false;
95 		return true;
96 	}
97 
98 	private static void detectMime(string name, ref Headers headers)
99 	{
100 		// Special case: .svgz
101 		if (name.endsWith(".svgz"))
102 			name = name[0 .. $ - 1] ~ ".gz";
103 
104 		if (name.endsWith(".gz"))
105 		{
106 			auto mimeType = guessMime(name[0 .. $-3]);
107 			if (mimeType)
108 			{
109 				headers["Content-Type"] = mimeType;
110 				headers["Content-Encoding"] = "gzip";
111 				return;
112 			}
113 		}
114 
115 		auto mimeType = guessMime(name);
116 		if (mimeType)
117 			headers["Content-Type"] = mimeType;
118 	}
119 
120 	/// Send a file from the disk
121 	HttpResponseEx serveFile(string path, string fsBase, bool enableIndex = false, string urlBase="/")
122 	{
123 		if (!checkPath(path))
124 		{
125 			writeError(HttpStatusCode.Forbidden);
126 			return this;
127 		}
128 
129 		assert(fsBase == "" || fsBase.endsWith("/"), "Invalid fsBase specified to serveFile");
130 		assert(urlBase.endsWith("/"), "Invalid urlBase specified to serveFile");
131 
132 		string filename = fsBase ~ path;
133 
134 		if (filename == "" || (filename.exists && filename.isDir))
135 		{
136 			if (filename.length && !filename.endsWith("/"))
137 				return redirect("/" ~ path ~ "/");
138 			else
139 			if (exists(filename ~ "index.html"))
140 				filename ~= "index.html";
141 			else
142 			if (!enableIndex)
143 			{
144 				writeError(HttpStatusCode.Forbidden);
145 				return this;
146 			}
147 			else
148 			{
149 				path = path.length ? path[0..$-1] : path;
150 				string title = `Directory listing of ` ~ encodeEntities(path=="" ? "/" : baseName(path));
151 
152 				auto segments = [urlBase[0..$-1]] ~ path.split("/");
153 				string segmentUrl;
154 				string html;
155 				foreach (i, segment; segments)
156 				{
157 					segmentUrl ~= (i ? encodeUrlParameter(segment) : segment) ~ "/";
158 					html ~= `<a style="margin-left: 5px" href="` ~ segmentUrl ~ `">` ~ encodeEntities(segment) ~ `/</a>`;
159 				}
160 
161 				html ~= `<ul>`;
162 				foreach (DirEntry de; dirEntries(filename, SpanMode.shallow))
163 				{
164 					auto name = baseName(de.name);
165 					auto suffix = de.isDir ? "/" : "";
166 					html ~= `<li><a href="` ~ encodeUrlParameter(name) ~ suffix ~ `">` ~ encodeEntities(name) ~ suffix ~ `</a></li>`;
167 				}
168 				html ~= `</ul>`;
169 				setStatus(HttpStatusCode.OK);
170 				writePage(title, html);
171 				return this;
172 			}
173 		}
174 
175 		if (!exists(filename) || !isFile(filename))
176 		{
177 			writeError(HttpStatusCode.NotFound);
178 			return this;
179 		}
180 
181 		detectMime(filename, headers);
182 
183 		headers["Last-Modified"] = httpTime(timeLastModified(filename));
184 		//data = [mapFile(filename, MmMode.read)];
185 		data = [readData(filename)];
186 		setStatus(HttpStatusCode.OK);
187 		return this;
188 	}
189 
190 	/// Fill a template using the given dictionary,
191 	/// substituting `"<?var?>"` with `dictionary["var"]`.
192 	static string parseTemplate(string data, string[string] dictionary)
193 	{
194 		import ae.utils.textout : StringBuilder;
195 		StringBuilder sb;
196 		while (true)
197 		{
198 			auto startpos = data.indexOf("<?");
199 			if(startpos==-1)
200 				break;
201 			auto endpos = data.indexOf("?>");
202 			if (endpos<startpos+2)
203 				throw new Exception("Bad syntax in template");
204 			string token = data[startpos+2 .. endpos];
205 			auto pvalue = token in dictionary;
206 			if(!pvalue)
207 				throw new Exception("Unrecognized token: " ~ token);
208 			sb.put(data[0 .. startpos], *pvalue);
209 			data = data[endpos+2 .. $];
210 		}
211 		sb.put(data);
212 		return sb.get();
213 	}
214 
215 	/// Load a template from the given file name,
216 	/// and fill it using the given dictionary.
217 	static string loadTemplate(string filename, string[string] dictionary)
218 	{
219 		return parseTemplate(readText(filename), dictionary);
220 	}
221 
222 	/// Serve `this.pageTemplate` as HTML, substituting `"<?title?>"`
223 	/// with `title`, `"<?content?>"` with `contentHTML`, and other
224 	/// tokens according to `pageTokens`.
225 	void writePageContents(string title, string contentHTML)
226 	{
227 		string[string] dictionary = pageTokens.dup;
228 		dictionary["title"] = encodeEntities(title);
229 		dictionary["content"] = contentHTML;
230 		data = [Data(parseTemplate(pageTemplate, dictionary))];
231 		headers["Content-Type"] = "text/html; charset=utf-8";
232 	}
233 
234 	/// Serve `this.pageTemplate` as HTML, substituting `"<?title?>"`
235 	/// with `title`, `"<?content?>"` with one `<p>` tag per `html`
236 	/// item, and other tokens according to `pageTokens`.
237 	void writePage(string title, string[] html ...)
238 	{
239 		if (!status)
240 			status = HttpStatusCode.OK;
241 
242 		string content;
243 		foreach (string p; html)
244 			content ~= "<p>" ~ p ~ "</p>\n";
245 
246 		string[string] dictionary;
247 		dictionary["title"] = encodeEntities(title);
248 		dictionary["content"] = content;
249 		writePageContents(title, parseTemplate(contentTemplate, dictionary));
250 	}
251 
252 	/// Return a likely reason (in English) for why a specified status code was served.
253 	static string getStatusExplanation(HttpStatusCode code)
254 	{
255 		switch(code)
256 		{
257 			case 400: return "The request could not be understood by the server due to malformed syntax.";
258 			case 401: return "You are not authorized to access this resource.";
259 			case 403: return "You have tried to access a restricted or unavailable resource, or attempted to circumvent server security.";
260 			case 404: return "The resource you are trying to access does not exist on the server.";
261 			case 405: return "The resource you are trying to access is not available using the method used in this request.";
262 
263 			case 500: return "An unexpected error has occured within the server software.";
264 			case 501: return "The resource you are trying to access represents unimplemented functionality.";
265 			default: return "";
266 		}
267 	}
268 
269 	/// Serve a nice error page using `this.errorTemplate`,
270 	/// `this.errorTokens`, and `writePageContents`.
271 	HttpResponseEx writeError(HttpStatusCode code, string details=null)
272 	{
273 		setStatus(code);
274 
275 		string[string] dictionary = errorTokens.dup;
276 		dictionary["code"] = to!string(cast(int)code);
277 		dictionary["message"] = encodeEntities(getStatusMessage(code));
278 		dictionary["explanation"] = encodeEntities(getStatusExplanation(code));
279 		dictionary["details"] = details ? "Error details:<br/><pre>" ~ encodeEntities(details) ~ "</pre>"  : "";
280 		string title = to!string(cast(int)code) ~ " - " ~ getStatusMessage(code);
281 		string html = parseTemplate(errorTemplate, dictionary);
282 
283 		writePageContents(title, html);
284 		return this;
285 	}
286 
287 	/// Set a `"Refresh"` header requesting a refresh after the given
288 	/// interval, optionally redirecting to another location.
289 	void setRefresh(int seconds, string location=null)
290 	{
291 		auto refresh = to!string(seconds);
292 		if (location)
293 			refresh ~= ";URL=" ~ location;
294 		headers["Refresh"] = refresh;
295 	}
296 
297 	/// Apply `disableCache` on this response's headers.
298 	void disableCache()
299 	{
300 		.disableCache(headers);
301 	}
302 
303 	/// Apply `cacheForever` on this response's headers.
304 	void cacheForever()
305 	{
306 		.cacheForever(headers);
307 	}
308 
309 	/// Construct and return a copy of this `HttpResponseEx`.
310 	HttpResponseEx dup()
311 	{
312 		auto c = new HttpResponseEx;
313 		c.status = this.status;
314 		c.statusMessage = this.statusMessage;
315 		c.headers = this.headers.dup;
316 		c.data = this.data.dup;
317 		return c;
318 	}
319 
320 	/**
321 	   Request a username and password.
322 
323 	   Usage:
324 	   ---
325 	   if (!response.authorize(request,
326 	                           (username, password) => username == "JohnSmith" && password == "hunter2"))
327 	       return conn.serveResponse(response);
328 	   ---
329 	*/
330 	bool authorize(HttpRequest request, bool delegate(string username, string password) authenticator)
331 	{
332 		bool check()
333 		{
334 			auto authorization = request.headers.get("Authorization", null);
335 			if (!authorization)
336 				return false; // No authorization header
337 			if (!authorization.skipOver("Basic "))
338 				return false; // Unknown authentication algorithm
339 			try
340 				authorization = cast(string)Base64.decode(authorization);
341 			catch (Base64Exception)
342 				return false; // Bad encoding
343 			auto parts = authorization.findSplit(":");
344 			if (!parts[1].length)
345 				return false; // Bad username/password formatting
346 			auto username = parts[0];
347 			auto password = parts[2];
348 			if (!authenticator(username, password))
349 				return false; // Unknown username/password
350 			return true;
351 		}
352 
353 		if (!check())
354 		{
355 			headers["WWW-Authenticate"] = `Basic`;
356 			writeError(HttpStatusCode.Unauthorized);
357 			return false;
358 		}
359 		return true;
360 	}
361 
362 	/// The default page template, used for `writePage` and error pages.
363 	static pageTemplate =
364 `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
365 
366 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
367   <head>
368     <title><?title?></title>
369     <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
370     <style type="text/css">
371       body
372       {
373         padding: 0;
374         margin: 0;
375         border-width: 0;
376         font-family: Tahoma, sans-serif;
377       }
378     </style>
379   </head>
380   <body>
381    <div style="background-color: #FFBFBF; width: 100%; height: 75px;">
382     <div style="position: relative; left: 150px; width: 300px; color: black; font-weight: bold; font-size: 30px;">
383      <span style="color: #FF0000; font-size: 65px;">D</span>HTTP
384     </div>
385    </div>
386    <div style="background-color: #FFC7C7; width: 100%; height: 4px;"></div>
387    <div style="background-color: #FFCFCF; width: 100%; height: 4px;"></div>
388    <div style="background-color: #FFD7D7; width: 100%; height: 4px;"></div>
389    <div style="background-color: #FFDFDF; width: 100%; height: 4px;"></div>
390    <div style="background-color: #FFE7E7; width: 100%; height: 4px;"></div>
391    <div style="background-color: #FFEFEF; width: 100%; height: 4px;"></div>
392    <div style="background-color: #FFF7F7; width: 100%; height: 4px;"></div>
393    <div style="position: relative; top: 40px; left: 10%; width: 80%;">
394 <?content?>
395    </div>
396   </body>
397 </html>`;
398 
399 	/// Additional variables to use when filling out page templates.
400 	string[string] pageTokens;
401 
402 	/// The default template for the page's contents, used for
403 	/// `writePage`.
404 	static contentTemplate =
405 `    <p><span style="font-weight: bold; font-size: 40px;"><?title?></span></p>
406 <?content?>
407 `;
408 
409 	/// The default template for error messages, used for `writeError`.
410 	static errorTemplate =
411 `    <p><span style="font-weight: bold; font-size: 40px;"><span style="color: #FF0000; font-size: 100px;"><?code?></span>(<?message?>)</span></p>
412     <p><?explanation?></p>
413     <p><?details?></p>
414 `;
415 
416 	/// Additional variables to use when filling out error templates.
417 	string[string] errorTokens;
418 }