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 }