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.exception; 18 import std.string; 19 import std.conv; 20 import std.file; 21 import std.path; 22 23 public import ae.net.http.common; 24 import ae.sys.data; 25 import ae.sys.dataio; 26 import ae.utils.array; 27 import ae.utils.json; 28 import ae.utils.xml; 29 import ae.utils.text; 30 import ae.utils.mime; 31 32 /// HttpResponse with some code to ease creating responses 33 final class HttpResponseEx : HttpResponse 34 { 35 public: 36 /// Redirect the UA to another location 37 HttpResponseEx redirect(string location, HttpStatusCode status = HttpStatusCode.SeeOther) 38 { 39 setStatus(status); 40 headers["Location"] = location; 41 return this; 42 } 43 44 HttpResponseEx serveData(string data, string contentType = "text/html; charset=utf-8") 45 { 46 return serveData(Data(data), contentType); 47 } 48 49 HttpResponseEx serveData(Data[] data, string contentType) 50 { 51 setStatus(HttpStatusCode.OK); 52 headers["Content-Type"] = contentType; 53 this.data = data; 54 return this; 55 } 56 57 HttpResponseEx serveData(Data data, string contentType) 58 { 59 return serveData([data], contentType); 60 } 61 62 string jsonCallback; 63 HttpResponseEx serveJson(T)(T v) 64 { 65 string data = toJson(v); 66 if (jsonCallback) 67 return serveData(jsonCallback~'('~data~')', "text/javascript"); 68 else 69 return serveData(data, "application/json"); 70 } 71 72 HttpResponseEx serveText(string data) 73 { 74 return serveData(Data(data), "text/plain; charset=utf-8"); 75 } 76 77 static bool checkPath(string path) 78 { 79 if (path.length && (path.contains("..") || path[0]=='/' || path[0]=='\\' || path.contains("//") || path.contains("\\\\"))) 80 return false; 81 return true; 82 } 83 84 /// Send a file from the disk 85 HttpResponseEx serveFile(string path, string fsBase, bool enableIndex = false, string urlBase="/") 86 { 87 if (!checkPath(path)) 88 { 89 writeError(HttpStatusCode.Forbidden); 90 return this; 91 } 92 93 assert(fsBase=="" || fsBase.endsWith("/"), "Invalid fsBase specified to serveFile"); 94 assert(urlBase.endsWith("/"), "Invalid urlBase specified to serveFile"); 95 96 string filename = fsBase ~ path; 97 98 if ((filename=="" || isDir(filename))) 99 { 100 if (filename.length && !filename.endsWith("/")) 101 return redirect("/" ~ path ~ "/"); 102 else 103 if (exists(filename ~ "index.html")) 104 filename ~= "index.html"; 105 else 106 if (!enableIndex) 107 { 108 writeError(HttpStatusCode.Forbidden); 109 return this; 110 } 111 else 112 { 113 path = path.length ? path[0..$-1] : path; 114 string title = `Directory listing of ` ~ encodeEntities(path=="" ? "/" : baseName(path)); 115 116 auto segments = [urlBase[0..$-1]] ~ path.split("/"); 117 string segmentUrl; 118 string html; 119 foreach (i, segment; segments) 120 { 121 segmentUrl ~= (i ? encodeUrlParameter(segment) : segment) ~ "/"; 122 html ~= `<a style="margin-left: 5px" href="` ~ segmentUrl ~ `">` ~ encodeEntities(segment) ~ `/</a>`; 123 } 124 125 html ~= `<ul>`; 126 foreach (DirEntry de; dirEntries(filename, SpanMode.shallow)) 127 { 128 auto name = baseName(de.name); 129 auto suffix = de.isDir ? "/" : ""; 130 html ~= `<li><a href="` ~ encodeUrlParameter(name) ~ suffix ~ `">` ~ encodeEntities(name) ~ suffix ~ `</a></li>`; 131 } 132 html ~= `</ul>`; 133 writePage(title, html); 134 return this; 135 } 136 } 137 138 if (!exists(filename) || !isFile(filename)) 139 { 140 writeError(HttpStatusCode.NotFound); 141 return this; 142 } 143 144 auto mimeType = guessMime(filename); 145 if (mimeType) 146 headers["Content-Type"] = mimeType; 147 148 headers["Last-Modified"] = httpTime(timeLastModified(filename)); 149 //data = [mapFile(filename, MmMode.read)]; 150 data = [readData(filename)]; 151 setStatus(HttpStatusCode.OK); 152 return this; 153 } 154 155 static string loadTemplate(string filename, string[string] dictionary) 156 { 157 return parseTemplate(readText(filename), dictionary); 158 } 159 160 static string parseTemplate(string data, string[string] dictionary) 161 { 162 import ae.utils.textout; 163 StringBuilder sb; 164 for(;;) 165 { 166 auto startpos = data.indexOf("<?"); 167 if(startpos==-1) 168 break; 169 auto endpos = data.indexOf("?>"); 170 if (endpos<startpos+2) 171 throw new Exception("Bad syntax in template"); 172 string token = data[startpos+2 .. endpos]; 173 auto pvalue = token in dictionary; 174 if(!pvalue) 175 throw new Exception("Unrecognized token: " ~ token); 176 sb.put(data[0 .. startpos], *pvalue); 177 data = data[endpos+2 .. $]; 178 } 179 sb.put(data); 180 return sb.get(); 181 } 182 183 void writePageContents(string title, string contentHTML) 184 { 185 string[string] dictionary; 186 dictionary["title"] = encodeEntities(title); 187 dictionary["content"] = contentHTML; 188 data = [Data(parseTemplate(pageTemplate, dictionary))]; 189 headers["Content-Type"] = "text/html; charset=utf-8"; 190 } 191 192 void writePage(string title, string[] html ...) 193 { 194 if (!status) 195 status = HttpStatusCode.OK; 196 197 string content; 198 foreach (string p; html) 199 content ~= "<p>" ~ p ~ "</p>\n"; 200 201 string[string] dictionary; 202 dictionary["title"] = encodeEntities(title); 203 dictionary["content"] = content; 204 writePageContents(title, parseTemplate(contentTemplate, dictionary)); 205 } 206 207 static string getStatusExplanation(HttpStatusCode code) 208 { 209 switch(code) 210 { 211 case 400: return "The request could not be understood by the server due to malformed syntax."; 212 case 401: return "You are not authorized to access this resource."; 213 case 403: return "You have tried to access a restricted or unavailable resource, or attempted to circumvent server security."; 214 case 404: return "The resource you are trying to access does not exist on the server."; 215 216 case 500: return "An unexpected error has occured within the server software."; 217 case 501: return "The resource you are trying to access represents an unimplemented functionality."; 218 default: return ""; 219 } 220 } 221 222 HttpResponseEx writeError(HttpStatusCode code, string details=null) 223 { 224 setStatus(code); 225 226 string[string] dictionary; 227 dictionary["code"] = to!string(cast(int)code); 228 dictionary["message"] = encodeEntities(getStatusMessage(code)); 229 dictionary["explanation"] = encodeEntities(getStatusExplanation(code)); 230 dictionary["details"] = details ? "Error details:<br/><pre>" ~ encodeEntities(details) ~ "</pre>" : ""; 231 string title = to!string(cast(int)code) ~ " - " ~ getStatusMessage(code); 232 string html = parseTemplate(errorTemplate, dictionary); 233 234 writePageContents(title, html); 235 return this; 236 } 237 238 void setRefresh(int seconds, string location=null) 239 { 240 auto refresh = to!string(seconds); 241 if (location) 242 refresh ~= ";URL=" ~ location; 243 headers["Refresh"] = refresh; 244 } 245 246 void disableCache() 247 { 248 .disableCache(headers); 249 } 250 251 void cacheForever() 252 { 253 .cacheForever(headers); 254 } 255 256 HttpResponseEx dup() 257 { 258 auto c = new HttpResponseEx; 259 c.status = this.status; 260 c.statusMessage = this.statusMessage; 261 c.headers = this.headers.dup; 262 c.data = this.data.dup; 263 return c; 264 } 265 266 static pageTemplate = 267 `<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> 268 269 <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> 270 <head> 271 <title><?title?></title> 272 <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/> 273 <style type="text/css"> 274 body 275 { 276 padding: 0; 277 margin: 0; 278 border-width: 0; 279 font-family: Tahoma, sans-serif; 280 } 281 </style> 282 </head> 283 <body> 284 <div style="background-color: #FFBFBF; width: 100%; height: 75px;"> 285 <div style="position: relative; left: 150px; width: 300px; color: black; font-weight: bold; font-size: 30px;"> 286 <span style="color: #FF0000; font-size: 65px;">D</span>HTTP 287 </div> 288 </div> 289 <div style="background-color: #FFC7C7; width: 100%; height: 4px;"></div> 290 <div style="background-color: #FFCFCF; width: 100%; height: 4px;"></div> 291 <div style="background-color: #FFD7D7; width: 100%; height: 4px;"></div> 292 <div style="background-color: #FFDFDF; width: 100%; height: 4px;"></div> 293 <div style="background-color: #FFE7E7; width: 100%; height: 4px;"></div> 294 <div style="background-color: #FFEFEF; width: 100%; height: 4px;"></div> 295 <div style="background-color: #FFF7F7; width: 100%; height: 4px;"></div> 296 <div style="position: relative; top: 40px; left: 10%; width: 80%;"> 297 <?content?> 298 </div> 299 </body> 300 </html>`; 301 302 static contentTemplate = 303 ` <p><span style="font-weight: bold; font-size: 40px;"><?title?></span></p> 304 <?content?> 305 `; 306 307 static errorTemplate = 308 ` <p><span style="font-weight: bold; font-size: 40px;"><span style="color: #FF0000; font-size: 100px;"><?code?></span>(<?message?>)</span></p> 309 <p><?explanation?></p> 310 <p><?details?></p> 311 `; 312 }