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 }