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 }