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