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