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