1 // Written in D programming language
2 /**
3 *
4 * Contains http logic
5 *
6 * Copyright: © 2014 DSoftOut
7 * License: Subject to the terms of the MIT license, as written in the included LICENSE file.
8 * Authors: Zaramzan <shamyan.roman@gmail.com>
9 *
10 */
11 module server.server;
12 
13 import core.atomic;
14 
15 import std.base64;
16 import std.string;
17 import std.exception;
18 import std.stdio;
19 import std.file;
20 import std.functional;
21 import std.path;
22 
23 import vibe.data.bson;
24 import vibe.http.server;
25 import vibe.http.router;
26 import vibe.core.driver;
27 import vibe.core.core;
28 import vibe.core.log : setLogLevel, setLogFile, LogLevel;
29 
30 import json_rpc.request;
31 import json_rpc.error;
32 import json_rpc.response;
33 
34 import server.database;
35 import server.config;
36 import server.options;
37 
38 import util;
39 import dlogg.log;
40 import dlogg.strict;
41 
42 /**
43 * Main program class
44 *
45 * Warning:
46 *   Don't run at once more than one instance of server. Workaround for vibe.d lack of
47 *   handler removing relies on that there is one running server at current moment.
48 *
49 * Authors: Zaramzan <shamyan.roman@gmail.com>
50 */
51 shared class Application
52 {
53 	/// Construct Application from ILogger, Options and AppConfig
54 	this(shared ILogger logger, immutable Options options, immutable AppConfig config)
55 	{
56 		this.mLogger = logger;
57 		this.options = options;
58 		this.appConfig = config;
59 		
60 		mSettings[this] = new HTTPServerSettings;
61         mRouters[this] = new URLRouter;
62 	}
63 	
64 	/// runs the server
65 	int run()
66 	in
67 	{
68 	    assert(!finalized, "Application was finalized!");
69 	}
70 	body
71 	{	
72 		if (running) return 0;
73 		
74 		logger.logInfo("Running server...");
75 		
76 		return startServer();
77 	}
78 	
79 	/// restart the server
80 	/**
81 	*  Recreates new application object with refreshed config.
82 	*  New app reuses old logger, old application is terminated.
83 	*/
84 	shared(Application) restart()
85     in
86     {
87         assert(!finalized, "Application was finalized!");
88     }
89     body
90 	{	
91         try
92         {
93             auto newConfig = immutable AppConfig(options.configName);
94 
95             finalize(false);
96             
97             return new shared Application(mLogger, options, newConfig);
98         }
99         catch(InvalidConfig e)
100         {
101             logger.logError(text("Configuration file at '", e.confPath, "' is invalid! ", e.msg));
102             assert(false);
103         }
104         catch(Exception e)
105         {
106             logger.logError(text("Failed to load configuration file at '", options.configName, "'! Details: ", e.msg));
107             assert(false);
108         }
109 	}
110 	
111 	/**
112 	*  Stops the server from any thread.
113 	*  Params:
114 	*  finalizeLog = if true, then inner logger wouldn't be finalized
115 	*/
116 	void finalize(bool finalizeLog = true)
117 	{
118 	    if(finalized) return;
119 	    
120 	    scope(exit) 
121 	    {
122 	        if(finalizeLog)
123 	            logger.finalize();
124 	        finalized = true;
125         }
126 		logger.logDebug("Called finalize");
127 		
128 		scope(failure)
129 		{
130 			logger.logError("Can not finalize server");
131 			
132 			return;
133 		}
134 		
135 		if (database)
136 		{
137 			database.finalize();
138 			logger.logDebug("Database pool is finalized");
139 		}
140 		
141 		if (running)
142 		{
143 			stopServer();
144 		}
145 		
146 		if(this in mSettings)
147 	    {
148 	        mSettings.remove(this);
149 	    }
150 	    if(this in mRouters)
151 	    {
152 	        mRouters.remove(this);
153 	    }
154 	}
155 	
156 	/// Return current application logger
157 	shared(ILogger) logger()
158     in
159     {
160         assert(!finalized, "Application was finalized!");
161     }
162     body
163 	{
164 	    return mLogger;
165 	}
166     
167     private:
168     
169 	void setupSettings()
170 	{
171 		settings.port = appConfig.port;
172 		
173 		settings.options = HTTPServerOption.none;
174 			
175 		if (appConfig.hostname) 
176 			settings.hostName = toUnqual(appConfig.hostname.idup);
177 			
178 		if (appConfig.bindAddresses)
179 			settings.bindAddresses =cast(string[]) appConfig.bindAddresses.idup;
180 		
181 		setLogLevel(LogLevel.none);
182 		setLogFile(appConfig.vibelog, LogLevel.info);
183 		setLogFile(appConfig.vibelog, LogLevel.error);
184 		setLogFile(appConfig.vibelog, LogLevel.warn);
185 	}
186 	
187 	void setupRouter()
188 	{
189 		auto del = cast(HTTPServerRequestDelegate) toDelegate(&handler);
190 		
191 		router.any("*", del);
192 	}
193 	
194 	void setupDatabase()
195 	{
196 		database = new shared Database(logger, appConfig);
197 		
198 		database.setupPool();
199 	}
200 	
201 	void configure()
202 	{	
203 		setupDatabase();
204 		
205 		try
206 		{
207 			database.loadJsonSqlTable();
208 		}
209 		catch(Throwable e)
210 		{
211 			logger.logError("Server error: "~e.msg);
212 			
213 			logger.logDebug("Server error:" ~ to!string(e));
214 			
215 			internalError = true;
216 		}
217 
218 		database.createCache();
219 		setupSettings();
220 		setupRouter();
221 	}
222 	
223 	int startServer()
224 	{
225 		try
226 		{	
227 			configure();
228 
229 			listenHTTP(settings, router);
230 			lowerPrivileges();
231 		
232 			logger.logInfo("Starting event loop");
233 			
234 			running = true;
235 			currApplication = this;
236 			return runEventLoop();
237 		}
238 		catch(Throwable e)
239 		{
240 			logger.logError("Server error: "~e.msg);
241 			logger.logDebug("Server error:" ~ to!string(e));
242 			
243 			finalize();
244 			return -1;
245 		}
246 		
247 	}
248 	
249 	void stopServer()
250 	{
251 		logger.logInfo("Stopping event loop");
252 		getEventDriver().exitEventLoop();
253 		running = false;
254 	}
255 	
256 	bool ifMaxConn()
257 	{
258 		return conns > appConfig.maxConn;
259 	}
260 	
261 	bool hasAuth(HTTPServerRequest req, out string user, out string password)
262 	{	
263 		auto pauth = "Authorization" in req.headers;
264 		
265 		if( pauth && (*pauth).startsWith("Basic ") )
266 		{
267 			string user_pw = cast(string)Base64.decode((*pauth)[6 .. $]);
268 	
269 			auto idx = user_pw.indexOf(":");
270 			enforce(idx >= 0, "Invalid auth string format!");
271 			user = user_pw[0 .. idx];
272 			password = user_pw[idx+1 .. $];
273 	
274 			
275 			return true;
276 		}
277 		
278 		return false;
279 	}
280 	
281 	/// handles HTTP requests
282 	static void handler(HTTPServerRequest req, HTTPServerResponse res)
283     in
284     {
285         assert(!currApplication.finalized, "Application was finalized!");
286     }
287     body
288 	{	
289 	    with(currApplication)
290 	    {
291     		atomicOp!"+="(conns, 1);
292     		
293     		scope(exit)
294     		{
295     			atomicOp!"-="(conns, 1);
296     		}
297     		
298     		enum CONTENT_TYPE = "application/json";
299     		
300     		if (ifMaxConn)
301     		{
302     			res.statusPhrase = "Reached maximum connections";
303     			throw new HTTPStatusException(HTTPStatus.serviceUnavailable,
304     				res.statusPhrase);
305     		}
306     		
307     		if (req.contentType != CONTENT_TYPE)
308     		{
309     			res.statusPhrase = "Supported only application/json content type";
310     			throw new HTTPStatusException(HTTPStatus.notImplemented,
311     				res.statusPhrase);
312     		}
313     		
314     		RpcRequest rpcReq;
315     		
316     		try
317     		{
318     			string jsonStr;
319     			
320     			jsonStr = cast(string) req.bodyReader.peek;
321     			
322     			rpcReq = RpcRequest(tryEx!RpcParseError(jsonStr));
323     			
324     			string user = null;
325     			string password = null;
326     			
327     			if (tryEx!RpcInvalidRequest(hasAuth(req, user, password)))
328     			{
329     				string[string] map;
330     				
331     				rpcReq.auth = map;
332     				
333     				enforceEx!RpcInvalidRequest(appConfig.sqlAuth.length >=2, "sqlAuth must have at least 2 elements");
334     				
335     				rpcReq.auth[appConfig.sqlAuth[0]] = user;
336     				
337     				rpcReq.auth[appConfig.sqlAuth[1]] = password;
338     			}
339     			else if (database.needAuth(rpcReq.method))
340     			{
341     				throw new HTTPStatusException(HTTPStatus.unauthorized);
342     			}
343     			
344     			// optional logging
345     			if(appConfig.logJsonQueries)
346     			{
347     				logger.logInfo(text("Received JSON-RPC request: ", rpcReq));
348     			}
349     			
350     			if (internalError)
351     			{				
352     				res.statusPhrase = "Failed to use table: "~appConfig.sqlJsonTable;
353     				
354     				throw new HTTPStatusException(HTTPStatus.internalServerError,
355     					res.statusPhrase);
356     			}
357 
358     			auto rpcRes = database.query(rpcReq);
359     			
360     			res.writeBody(rpcRes.toJson.toPrettyString, CONTENT_TYPE);
361     			
362     			void resetCacheIfNeeded()
363     			{
364     				yield();
365     				
366     				database.dropcaches(rpcReq.method);	
367     			}
368     			
369     			runTask(&resetCacheIfNeeded);
370     		
371     		}
372     		catch (RpcException ex)
373     		{
374     			RpcError error = RpcError(ex);
375     			
376     			RpcResponse rpcRes = RpcResponse(rpcReq.id, error);
377     			
378     			res.writeBody(rpcRes.toJson.toPrettyString, CONTENT_TYPE);
379     		}
380 		}
381 	}
382 	
383 	HTTPServerSettings settings()
384 	{
385 	    assert(this in mSettings);
386 	    return mSettings[this];
387 	}
388 	
389 	void settings(HTTPServerSettings value)
390 	{
391 	    assert(this in mSettings);
392 	    mSettings[this] = value;
393 	}
394 	
395 	URLRouter router()
396 	{
397 	    assert(this in mRouters);
398 	    return mRouters[this];
399 	}
400 	
401 	void router(URLRouter value)
402 	{
403 	    assert(this in mRouters);
404 	    mRouters[this] = value;
405 	}
406 	
407 	ILogger mLogger;
408 	Database database;
409 	
410 	immutable AppConfig appConfig;
411 	immutable Options options;
412 	
413 	int conns;
414 	bool running;
415 	bool internalError;
416 	bool finalized;
417 	
418 	private
419 	{
420 		__gshared HTTPServerSettings[shared const Application] mSettings;
421 		__gshared URLRouter[shared const Application] mRouters;
422 		static shared Application currApplication;
423 	}
424 }