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