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