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