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.exception;
18 import std.string;
19 import std.conv;
20 import std.file;
21 import std.path;
22 
23 public import ae.net.http.common;
24 import ae.sys.data;
25 import ae.sys.dataio;
26 import ae.utils.array;
27 import ae.utils.json;
28 import ae.utils.xml;
29 import ae.utils.text;
30 import ae.utils.mime;
31 
32 /// HttpResponse with some code to ease creating responses
33 final class HttpResponseEx : HttpResponse
34 {
35 public:
36 	/// Redirect the UA to another location
37 	HttpResponseEx redirect(string location, HttpStatusCode status = HttpStatusCode.SeeOther)
38 	{
39 		setStatus(status);
40 		headers["Location"] = location;
41 		return this;
42 	}
43 
44 	HttpResponseEx serveData(string data, string contentType = "text/html; charset=utf-8")
45 	{
46 		return serveData(Data(data), contentType);
47 	}
48 
49 	HttpResponseEx serveData(Data[] data, string contentType)
50 	{
51 		setStatus(HttpStatusCode.OK);
52 		headers["Content-Type"] = contentType;
53 		this.data = data;
54 		return this;
55 	}
56 
57 	HttpResponseEx serveData(Data data, string contentType)
58 	{
59 		return serveData([data], contentType);
60 	}
61 
62 	string jsonCallback;
63 	HttpResponseEx serveJson(T)(T v)
64 	{
65 		string data = toJson(v);
66 		if (jsonCallback)
67 			return serveData(jsonCallback~'('~data~')', "text/javascript");
68 		else
69 			return serveData(data, "application/json");
70 	}
71 
72 	HttpResponseEx serveText(string data)
73 	{
74 		return serveData(Data(data), "text/plain; charset=utf-8");
75 	}
76 
77 	static bool checkPath(string path)
78 	{
79 		if (path.length && (path.contains("..") || path[0]=='/' || path[0]=='\\' || path.contains("//") || path.contains("\\\\")))
80 			return false;
81 		return true;
82 	}
83 
84 	/// Send a file from the disk
85 	HttpResponseEx serveFile(string path, string fsBase, bool enableIndex = false, string urlBase="/")
86 	{
87 		if (!checkPath(path))
88 		{
89 			writeError(HttpStatusCode.Forbidden);
90 			return this;
91 		}
92 
93 		assert(fsBase=="" || fsBase.endsWith("/"), "Invalid fsBase specified to serveFile");
94 		assert(urlBase.endsWith("/"), "Invalid urlBase specified to serveFile");
95 
96 		string filename = fsBase ~ path;
97 
98 		if ((filename=="" || isDir(filename)))
99 		{
100 			if (filename.length && !filename.endsWith("/"))
101 				return redirect("/" ~ path ~ "/");
102 			else
103 			if (exists(filename ~ "index.html"))
104 				filename ~= "index.html";
105 			else
106 			if (!enableIndex)
107 			{
108 				writeError(HttpStatusCode.Forbidden);
109 				return this;
110 			}
111 			else
112 			{
113 				path = path.length ? path[0..$-1] : path;
114 				string title = `Directory listing of ` ~ encodeEntities(path=="" ? "/" : baseName(path));
115 
116 				auto segments = [urlBase[0..$-1]] ~ path.split("/");
117 				string segmentUrl;
118 				string html;
119 				foreach (i, segment; segments)
120 				{
121 					segmentUrl ~= (i ? encodeUrlParameter(segment) : segment) ~ "/";
122 					html ~= `<a style="margin-left: 5px" href="` ~ segmentUrl ~ `">` ~ encodeEntities(segment) ~ `/</a>`;
123 				}
124 
125 				html ~= `<ul>`;
126 				foreach (DirEntry de; dirEntries(filename, SpanMode.shallow))
127 				{
128 					auto name = baseName(de.name);
129 					auto suffix = de.isDir ? "/" : "";
130 					html ~= `<li><a href="` ~ encodeUrlParameter(name) ~ suffix ~ `">` ~ encodeEntities(name) ~ suffix ~ `</a></li>`;
131 				}
132 				html ~= `</ul>`;
133 				writePage(title, html);
134 				return this;
135 			}
136 		}
137 
138 		if (!exists(filename) || !isFile(filename))
139 		{
140 			writeError(HttpStatusCode.NotFound);
141 			return this;
142 		}
143 
144 		auto mimeType = guessMime(filename);
145 		if (mimeType)
146 			headers["Content-Type"] = mimeType;
147 
148 		headers["Last-Modified"] = httpTime(timeLastModified(filename));
149 		//data = [mapFile(filename, MmMode.read)];
150 		data = [readData(filename)];
151 		setStatus(HttpStatusCode.OK);
152 		return this;
153 	}
154 
155 	static string loadTemplate(string filename, string[string] dictionary)
156 	{
157 		return parseTemplate(readText(filename), dictionary);
158 	}
159 
160 	static string parseTemplate(string data, string[string] dictionary)
161 	{
162 		import ae.utils.textout;
163 		StringBuilder sb;
164 		for(;;)
165 		{
166 			auto startpos = data.indexOf("<?");
167 			if(startpos==-1)
168 				break;
169 			auto endpos = data.indexOf("?>");
170 			if (endpos<startpos+2)
171 				throw new Exception("Bad syntax in template");
172 			string token = data[startpos+2 .. endpos];
173 			auto pvalue = token in dictionary;
174 			if(!pvalue)
175 				throw new Exception("Unrecognized token: " ~ token);
176 			sb.put(data[0 .. startpos], *pvalue);
177 			data = data[endpos+2 .. $];
178 		}
179 		sb.put(data);
180 		return sb.get();
181 	}
182 
183 	void writePageContents(string title, string contentHTML)
184 	{
185 		string[string] dictionary;
186 		dictionary["title"] = encodeEntities(title);
187 		dictionary["content"] = contentHTML;
188 		data = [Data(parseTemplate(pageTemplate, dictionary))];
189 		headers["Content-Type"] = "text/html; charset=utf-8";
190 	}
191 
192 	void writePage(string title, string[] html ...)
193 	{
194 		if (!status)
195 			status = HttpStatusCode.OK;
196 
197 		string content;
198 		foreach (string p; html)
199 			content ~= "<p>" ~ p ~ "</p>\n";
200 
201 		string[string] dictionary;
202 		dictionary["title"] = encodeEntities(title);
203 		dictionary["content"] = content;
204 		writePageContents(title, parseTemplate(contentTemplate, dictionary));
205 	}
206 
207 	static string getStatusExplanation(HttpStatusCode code)
208 	{
209 		switch(code)
210 		{
211 			case 400: return "The request could not be understood by the server due to malformed syntax.";
212 			case 401: return "You are not authorized to access this resource.";
213 			case 403: return "You have tried to access a restricted or unavailable resource, or attempted to circumvent server security.";
214 			case 404: return "The resource you are trying to access does not exist on the server.";
215 
216 			case 500: return "An unexpected error has occured within the server software.";
217 			case 501: return "The resource you are trying to access represents an unimplemented functionality.";
218 			default: return "";
219 		}
220 	}
221 
222 	HttpResponseEx writeError(HttpStatusCode code, string details=null)
223 	{
224 		setStatus(code);
225 
226 		string[string] dictionary;
227 		dictionary["code"] = to!string(cast(int)code);
228 		dictionary["message"] = encodeEntities(getStatusMessage(code));
229 		dictionary["explanation"] = encodeEntities(getStatusExplanation(code));
230 		dictionary["details"] = details ? "Error details:<br/><pre>" ~ encodeEntities(details) ~ "</pre>"  : "";
231 		string title = to!string(cast(int)code) ~ " - " ~ getStatusMessage(code);
232 		string html = parseTemplate(errorTemplate, dictionary);
233 
234 		writePageContents(title, html);
235 		return this;
236 	}
237 
238 	void setRefresh(int seconds, string location=null)
239 	{
240 		auto refresh = to!string(seconds);
241 		if (location)
242 			refresh ~= ";URL=" ~ location;
243 		headers["Refresh"] = refresh;
244 	}
245 
246 	void disableCache()
247 	{
248 		.disableCache(headers);
249 	}
250 
251 	void cacheForever()
252 	{
253 		.cacheForever(headers);
254 	}
255 
256 	HttpResponseEx dup()
257 	{
258 		auto c = new HttpResponseEx;
259 		c.status = this.status;
260 		c.statusMessage = this.statusMessage;
261 		c.headers = this.headers.dup;
262 		c.data = this.data.dup;
263 		return c;
264 	}
265 
266 	static pageTemplate =
267 `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
268 
269 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
270   <head>
271     <title><?title?></title>
272     <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
273     <style type="text/css">
274       body
275       {
276         padding: 0;
277         margin: 0;
278         border-width: 0;
279         font-family: Tahoma, sans-serif;
280       }
281     </style>
282   </head>
283   <body>
284    <div style="background-color: #FFBFBF; width: 100%; height: 75px;">
285     <div style="position: relative; left: 150px; width: 300px; color: black; font-weight: bold; font-size: 30px;">
286      <span style="color: #FF0000; font-size: 65px;">D</span>HTTP
287     </div>
288    </div>
289    <div style="background-color: #FFC7C7; width: 100%; height: 4px;"></div>
290    <div style="background-color: #FFCFCF; width: 100%; height: 4px;"></div>
291    <div style="background-color: #FFD7D7; width: 100%; height: 4px;"></div>
292    <div style="background-color: #FFDFDF; width: 100%; height: 4px;"></div>
293    <div style="background-color: #FFE7E7; width: 100%; height: 4px;"></div>
294    <div style="background-color: #FFEFEF; width: 100%; height: 4px;"></div>
295    <div style="background-color: #FFF7F7; width: 100%; height: 4px;"></div>
296    <div style="position: relative; top: 40px; left: 10%; width: 80%;">
297 <?content?>
298    </div>
299   </body>
300 </html>`;
301 
302 	static contentTemplate =
303 `    <p><span style="font-weight: bold; font-size: 40px;"><?title?></span></p>
304 <?content?>
305 `;
306 
307 	static errorTemplate =
308 `    <p><span style="font-weight: bold; font-size: 40px;"><span style="color: #FF0000; font-size: 100px;"><?code?></span>(<?message?>)</span></p>
309     <p><?explanation?></p>
310     <p><?details?></p>
311 `;
312 }