1 /// Written in the D programming language.
2 
3 module semitwistWeb.init;
4 
5 import std.getopt;
6 import std.stdio;
7 import core.memory;
8 
9 import vibe.vibe;
10 import vibe.core.args;
11 import vibe.core.connectionpool;
12 import mysql.db;
13 import sdlang.exception;
14 
15 import semitwist.util.all;
16 
17 import semitwistWeb.conf;
18 import semitwistWeb.db;
19 import semitwistWeb.doc;
20 import semitwistWeb.handler;
21 import semitwistWeb.session;
22 import semitwistWeb.util;
23 
24 bool initDB        = false;
25 bool clearSessions = false;
26 bool noCacheStatic = false;
27 bool noStatic      = false;
28 ushort port        = 8080;
29 string[] bindAddresses;
30 string logFile     = "";
31 
32 //TODO: Find/create a tool to monitor the logfile and send emails for each new entry.
33 
34 alias int function(ref HTTPServerSettings, ref URLRouter) CustomPostInit;
35 
36 // This is a modification of vibe.d's built-in main().
37 int semitwistWebMain(CustomSession, CustomHandler, UserDBOTypes...)
38 	(string[] args, CustomPostInit customPostInit, LockedConnection!Connection delegate() openDB)
39 {
40 	debug runDocHelperUnittest();
41 
42 	dbHelperOpenDB = openDB;
43 	BaseHandler.addAppContextCallback = &CustomHandler.addAppContext;
44 
45 	if(auto errlvl = processCustomCmdLine(args) != -1)
46 		return errlvl;
47 
48 	try
49 		loadConf();
50 	catch(SDLangException e)
51 	{
52 		stderr.writeln(e.msg);
53 		return 1;
54 	}
55 
56 	if(initDB)
57 	{
58 		initializeDB();
59 		return 0;
60 	}
61 	
62 	if(auto errlvl = init!(CustomSession, CustomHandler, UserDBOTypes)(customPostInit) != -1)
63 		return errlvl;
64 
65 	stLogInfo("Running event loop...");
66 	try {
67 		return runEventLoop();
68 	} catch( Throwable th ){
69 		stLogError("Unhandled exception in event loop: ", th);
70 		return 1;
71 	}
72 }
73 
74 /// Returns: -1 normally, or else errorlevel to exit with
75 private int processCustomCmdLine(ref string[] args)
76 {
77 	readOption("init-db",           &initDB,              "Init the DB and exit (THIS WILL DESTROY ALL DATA!)");
78 	readOption("clear-sessions",    &clearSessions,       "Upon startup, clear sessions in DB insetad of resuming them.");
79 	readOption("port",              &port,                "Port to bind.");
80 	string bindAddress;
81 	while(readOption("ip", &bindAddress, "IP address to bind. (Can be specified multiple times)"))
82 		bindAddresses ~= bindAddress;
83 	readOption("no-cache",          &BaseHandler.noCache, "Disable internal page caching. (Useful during development)");
84 	readOption("no-cache-static",   &noCacheStatic,       "Set HTTP headers on static files to disable caching. (Useful during development)");
85 	readOption("no-static",         &noStatic,            "Disable serving of static files.");
86 	readOption("log",               &logFile,             "Set logfile.");
87 
88 	readOption("insecure",          &BaseHandler.allowInsecure,   "Allow non-HTTPS requests. Implies --insecure-cookies");
89 	readOption("insecure-cookies",  &useInsecureCookies,          "Don't set SECURE attribute on session cookies.");
90 	readOption("public-debug-info", &BaseHandler.publicDebugInfo, "Display uncaught exceptions and stack traces to user. (Useful during development)");
91 	readOption("log-sql",           &dbHelperLogSql,              "Log all SQL statements executed. (Useful during development)");
92 	
93 	try
94 	{
95 		if(!finalizeCommandLineOptions())
96 			return 0;
97 	}
98 	catch(Exception e)
99 	{
100 		stLogError("Error processing command line: ", e.msg);
101 		return 1;
102 	}
103 	
104 	if(bindAddresses.length == 0)
105 		bindAddresses = ["0.0.0.0", "::"];
106 
107 	if(BaseHandler.allowInsecure)
108 		useInsecureCookies = true;
109 	
110 	setLogFormat(FileLogger.Format.threadTime);
111 	if(logFile != "")
112 		setLogFile(logFile, LogLevel.info);
113 	
114 	return -1;
115 }
116 
117 /// Returns: -1 normally, or else errorlevel to exit with
118 private int init(CustomSession, CustomHandler, UserDBOTypes...)
119 	(CustomPostInit customPostInit)
120 {
121 	version(RequireSecure)
122 	{
123 		if(Handler.allowInsecure || useInsecureCookies || Handler.publicDebugInfo)
124 		{
125 			stLogError("This was compiled with -version=RequireSecure, therefore the following flags are disabled: --insecure --insecure-cookies --public-debug-info");
126 			return 1;
127 		}
128 	}
129 	
130 	// Warn about --insecure-cookies
131 	if(useInsecureCookies)
132 		stLogWarn("Used --insecure-cookies: INSECURE cookies are ON! Session cookies will not use the Secure attribute!");
133 
134 	// Warn about --insecure
135 	if(BaseHandler.allowInsecure)
136 		stLogWarn("Used --insecure: INSECURE mode is ON! HTTPS will NOT be forced!");
137 
138 	// Warn about HTTP
139 	if(conf.host.toLower().startsWith("http://"))
140 	{
141 		if(BaseHandler.allowInsecure)
142 			stLogWarn(
143 				"Non-relative URLs are set to HTTP, not HTTPS! ",
144 				"If you did not intend this, change conf.host and recompile."
145 			);
146 		else
147 		{
148 			// Require --insecure for non-HTTPS
149 			stLogError(
150 				"conf.host is HTTP instead of HTTPS. THIS IS NOT RECOMMENDED. ",
151 				"If you wish to allow this anyway, you must use the --insecure flag. ",
152 				"Note that this will cause non-relative application URLs to be HTTP instead of HTTPS."
153 			);
154 			return 1;
155 		}
156 	}
157 	
158 	{
159 		scope(failure)
160 		{
161 			stLogError(
162 				"There was an error building the basic pages.\n",
163 				import("dbTroubleshootMsg.txt")
164 			);
165 		}
166 
167 		auto dbConn = dbHelperOpenDB();
168 
169 		stLogInfo("Preloading db cache...");
170 		rebuildDBCache!UserDBOTypes(dbConn);
171 
172 		if(clearSessions)
173 		{
174 			stLogInfo("Clearing persistent sessions...");
175 			SessionDB.dbDeleteAll(dbConn);
176 			return 0;
177 		}
178 
179 		stLogInfo("Restoring sessions...");
180 		sessionStore = new MemorySessionStore();
181 		restoreSessions!CustomSession(dbConn);
182 	}
183 	
184 	stLogInfo("Initing HTTP server settings...");
185 	alias handlerDispatchError!CustomHandler customHandlerDispatchError;
186 	auto httpServerSettings = new HTTPServerSettings();
187 	httpServerSettings.port = port;
188 	httpServerSettings.bindAddresses = bindAddresses;
189 	httpServerSettings.sessionStore = sessionStore;
190 	httpServerSettings.errorPageHandler =
191 		(req, res, err) => customHandlerDispatchError!"errorHandler"(req, res, err);
192 	
193 	stLogInfo("Initing URL router...");
194 	auto router = initRouter!CustomHandler();
195 	
196 	if(customPostInit !is null)
197 	{
198 		stLogInfo("Running customPostInit...");
199 		if(auto errlvl = customPostInit(httpServerSettings, router) != -1)
200 			return errlvl;
201 	}
202 	
203 	stLogInfo("Forcing GC cycle...");
204 	GC.collect();
205 	GC.minimize();
206 
207 	stLogInfo("Done initing SemiTwist Web Framework");
208 	listenHTTP(httpServerSettings, router);
209 	return -1;
210 }
211 
212 private URLRouter initRouter(CustomHandler)()
213 {
214 	auto router = new URLRouter();
215 
216 	foreach(pageName; PageBase.getNames())
217 		router.addPage( PageBase.get(pageName) );
218 	
219 	/// If you're serving the static files directly (for example, through a
220 	/// reverse proxy), you can prevent this application from serving them
221 	/// with --no-static
222 	if(!noStatic)
223 	{
224 		auto localPath = getExecPath ~ conf.staticsRealPath;
225 
226 		auto fss = new HTTPFileServerSettings();
227 		//fss.failIfNotFound   = true; // Setting isn't in latest vibe.d
228 		fss.serverPathPrefix = conf.staticsUrl;
229 
230 		if(noCacheStatic)
231 		{
232 			fss.maxAge           = seconds(1);
233 			fss.preWriteCallback = (scope req, scope res, ref physicalPath) {
234 				res.headers.remove("Etag");
235 				res.headers["Cache-Control"] = "no-store";
236 			};
237 		}
238 		else
239 			fss.maxAge = hours(24);
240 		
241 		router.get(conf.staticsUrl~"*", serveStaticFiles(localPath, fss));
242 	}
243 
244 	alias handlerDispatch!CustomHandler customHandlerDispatch;
245 	router.get ("*", &customHandlerDispatch!"notFound");
246 	router.post("*", &customHandlerDispatch!"notFound");
247 	
248 	return router;
249 }