1 /// Written in the D programming language. 2 3 module semitwistWeb.handler; 4 5 import std.algorithm; 6 import std.array; 7 import std.conv; 8 import std.file; 9 import std.path; 10 import std.range; 11 import std.regex; 12 import std.stdio; 13 import std..string; 14 import std.traits; 15 import std.typecons; 16 import std.variant; 17 18 import vibe.vibe; 19 import mysql.db; 20 import semitwist.util.all; 21 22 import semitwistWeb.conf; 23 import semitwistWeb.db; 24 import semitwistWeb.doc; 25 import semitwistWeb.session; 26 import semitwistWeb.util; 27 28 template handlerDispatch(CustomHandler) 29 { 30 void handlerDispatch(string funcName)(HTTPServerRequest req, HTTPServerResponse res) 31 { 32 PageBase.validateLoginPageName(); 33 34 if(CustomHandler.noCache) 35 CustomHandler.clearDocsCache(); 36 37 // Force HTTPS 38 if(!CustomHandler.allowInsecure && !req.isSSLReverseProxy()) 39 { 40 if(!conf.host.toLower().startsWith("https://")) 41 throw new Exception("Internal Error: conf.host was expected to always be https whenever SSL is being forced!"); 42 43 string url; 44 if(req.queryString == "") 45 url = text(conf.host, req.path); 46 else 47 url = text(conf.host, req.path, "?", req.queryString); 48 49 auto page = CustomHandler(BaseHandler(req, res, null), null).redirect(url, HTTPStatus.TemporaryRedirect); 50 page.send(req, res, null); 51 return; 52 } 53 54 auto sess = CustomHandler.setupSession(req, res); 55 56 // Instantiate viewContext on the stack, it doesn't need to outlive this function. 57 mixin(createUnsafeScoped("Mustache.Context", "viewContext")); 58 59 auto handler = CustomHandler(BaseHandler(req, res, sess, viewContext), sess); 60 mixin("handler."~funcName~"().send(req, res, sess);"); 61 } 62 } 63 64 template handlerDispatchError(CustomHandler) 65 { 66 void handlerDispatchError(string funcName)(HTTPServerRequest req, HTTPServerResponse res, HTTPServerErrorInfo error) 67 { 68 if(CustomHandler.noCache) 69 CustomHandler.clearDocsCache(); 70 71 auto sess = CustomHandler.setupSession(req, res); 72 73 // Instantiate viewContext on the stack, it doesn't need to outlive this function. 74 mixin(createUnsafeScoped("Mustache.Context", "viewContext")); 75 76 auto handler = CustomHandler(BaseHandler(req, res, sess, viewContext), sess); 77 mixin("handler."~funcName~"(error).send(req, res, sess);"); 78 } 79 } 80 81 struct HttpResult 82 { 83 int statusCode; 84 string mime; 85 Algebraic!(string, const(ubyte)[]) content; 86 string locationHeader; 87 88 void send(HTTPServerRequest req, HTTPServerResponse res, SessionData sess) 89 { 90 sendImpl(req, res, sess, true); 91 } 92 93 private void sendImpl( 94 HTTPServerRequest req, HTTPServerResponse res, 95 SessionData sess, bool normalErrorPageOnFailure 96 ) 97 { 98 void fail() 99 { 100 if(normalErrorPageOnFailure) 101 { 102 // Attempt to send the normal 500 page 103 BaseHandler(req, res, sess) 104 .genericError(500) 105 .sendImpl(req, res, sess, false); 106 } 107 else 108 { 109 // Just send a bare-bones 500 page 110 sess.oneShotMessage = null; 111 res.statusCode = 500; 112 res.writeBody(BaseHandler.genericErrorMessage, "text/html"); 113 } 114 } 115 116 void setupHeaders() 117 { 118 res.statusCode = statusCode; 119 if(locationHeader != "") 120 res.headers["Location"] = locationHeader; 121 } 122 123 void clearOneShotMessage() 124 { 125 // If statusCode is 2xx, 4xx or 5xx 126 if((statusCode >= 200 && statusCode < 300) || statusCode >= 400) 127 sess.oneShotMessage = null; 128 } 129 130 if(!content.hasValue) 131 { 132 stLogError("HttpResult.content has no value. URL: ", req.path); 133 fail(); 134 } 135 else if(content.type == typeid(string)) 136 { 137 setupHeaders(); 138 clearOneShotMessage(); 139 res.writeBody(content.get!(string)(), mime); 140 } 141 else if(content.type == typeid( const(ubyte)[] )) 142 { 143 setupHeaders(); 144 clearOneShotMessage(); 145 res.writeBody(content.get!( const(ubyte)[] )(), mime); 146 } 147 else 148 { 149 stLogError("HttpResult.content contains an unexpected type"); 150 fail(); 151 } 152 } 153 } 154 155 struct BaseHandler 156 { 157 enum genericErrorMessage = "<p>Sorry, an error has occurred.</p>"; 158 159 static bool noCache = false; 160 static bool allowInsecure = false; 161 static bool publicDebugInfo = false; 162 static void function(Mustache.Context, SessionData) addAppContextCallback; 163 HTTPServerRequest req; 164 HTTPServerResponse res; 165 SessionData baseSess; 166 Mustache.Context viewContext; /// On the stack: DO NOT SAVE past lifetime of handlerDispatch. 167 168 HttpResult errorHandler(HTTPServerErrorInfo error) 169 { 170 try 171 { 172 if(error.code >= 400 && error.code < 500) 173 return errorHandler4xx(error); 174 175 if(error.code >= 500 && error.code < 600) 176 return errorHandler5xx(error); 177 178 logHttpError(error); 179 stLogWarn( 180 format( 181 "[%s] Unexpectedly handled \"error\" code outside 4xx/5xx: %s - %s. Sending 500 instead.", 182 req.clientIPs, error.code, httpStatusText(error.code) 183 ) 184 ); 185 186 return genericError(HTTPStatus.InternalServerError); 187 } 188 catch(Exception e) 189 { 190 stLogError("Uncaught exception during error handler: ", e); 191 192 // Just send a bare-bones 500 page 193 HttpResult r; 194 r.statusCode = 500; 195 r.mime = "text/html"; 196 r.content = BaseHandler.genericErrorMessage; 197 return r; 198 } 199 } 200 201 private void logHttpError(HTTPServerErrorInfo error) 202 { 203 stLogError( 204 format( 205 "[%s] %s - %s\n\n%s\n\nInternal error information:\n%s", 206 req.clientIPs, error.code, httpStatusText(error.code), 207 error.message, error.debugMessage 208 ) 209 ); 210 } 211 212 private HttpResult errorHandler4xx(HTTPServerErrorInfo error) 213 { 214 if(error.code == 400) 215 return badRequest(); 216 217 if(error.code == 404) 218 return notFound(); 219 220 return genericError(error.code); 221 } 222 223 private HttpResult errorHandler5xx(HTTPServerErrorInfo error) 224 { 225 logHttpError(error); 226 227 if(BaseHandler.publicDebugInfo) 228 { 229 auto errorMsg = 230 BaseHandler.genericErrorMessage ~ 231 `<pre class="pre-wrap" style="width: 100%;">` ~ 232 htmlEscapeMin(error.debugMessage) ~ 233 "</pre>"; 234 235 return genericError(error.code, errorMsg); 236 } 237 else 238 return genericError(error.code); 239 } 240 241 HttpResult genericError(int statusCode) 242 { 243 return genericError(statusCode, BaseHandler.genericErrorMessage); 244 } 245 246 HttpResult genericError(int statusCode, string message) 247 { 248 viewContext["errorCode"] = to!string(statusCode); 249 viewContext["errorString"] = httpStatusText(statusCode); 250 viewContext["errorMsg"] = message; 251 252 HttpResult r; 253 r.statusCode = statusCode; 254 r.mime = "text/html"; 255 r.content = renderPage("err-generic"); 256 return r; 257 } 258 259 HttpResult notFound() 260 { 261 HttpResult r; 262 r.statusCode = HTTPStatus.NotFound; 263 r.mime = "text/html"; 264 r.content = renderPage("err-not-found"); 265 return r; 266 } 267 268 HttpResult redirect(string url, int status = HTTPStatus.Found) 269 { 270 HttpResult r; 271 r.statusCode = status; 272 r.locationHeader = url; 273 r.mime = "text/plain"; 274 r.content = "Redirect to: " ~ url; 275 return r; 276 } 277 278 /// Automatically sets session's postLoginUrl to the URL requested by the user. 279 HttpResult redirectToLogin(string loginUrl) 280 { 281 baseSess.postLoginUrl = req.requestURL; 282 return redirect(loginUrl); 283 } 284 285 /// If postLoginUrl=="", that signals that your app's default 286 /// after-login URL is to be used. 287 HttpResult redirectToLogin(string loginUrl, string postLoginUrl) 288 { 289 baseSess.postLoginUrl = postLoginUrl; 290 return redirect(loginUrl); 291 } 292 293 /// Call this when the user succesfulyl logs in. 294 /// 295 /// If the user had attempted to reach a login-only URL, this redirects 296 /// them back to it. Otherwise, this redirects to 'defaultUrl'. 297 HttpResult redirectToPostLogin(string defaultUrl) 298 { 299 auto targetUrl = baseSess.postLoginUrl; 300 if(targetUrl == "") 301 targetUrl = defaultUrl; 302 303 baseSess.postLoginUrl = null; 304 return redirect(targetUrl); 305 } 306 307 HttpResult badRequest() 308 { 309 HttpResult r; 310 r.statusCode = HTTPStatus.BadRequest; 311 r.mime = "text/html"; 312 r.content = renderPage("err-bad-request"); 313 return r; 314 } 315 316 HttpResult ok(T)(T content, string mimeType) if( is(T:string) || is(T:const(ubyte)[]) ) 317 { 318 HttpResult r; 319 r.statusCode = HTTPStatus.OK; 320 r.mime = mimeType; 321 r.content = content; 322 return r; 323 } 324 325 HttpResult okHtml(PageBase page) 326 { 327 return okHtml(renderPage(page)); 328 } 329 330 HttpResult okHtml(string content) 331 { 332 return ok(content, "text/html"); 333 } 334 335 string renderPage(PageBase page) 336 { 337 return renderPage(page.viewName); 338 } 339 340 string renderPage(string templateName) 341 { 342 if(addAppContextCallback is null) 343 throw new Exception("'BaseHandler.addAppContextCallback' has not been set."); 344 345 addCommonContext(viewContext, baseSess); 346 addAppContextCallback(viewContext, baseSess); 347 return mustache.renderString(HtmlTemplateAccess[templateName], viewContext); 348 } 349 }