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