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 }