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