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