1 /** 2 * Copyright © DiamondMVC 2019 3 * License: MIT (https://github.com/DiamondMVC/Diamond/blob/master/LICENSE) 4 * Author: Jacob Jensen (bausshf) 5 */ 6 module diamond.core.logging; 7 8 import diamond.core.apptype; 9 10 static if (loggingEnabled) 11 { 12 import diamond.http; 13 14 /// Alias for a logging delegate. 15 private alias LogDelegate = void delegate(LogResult); 16 17 /// Wrapper around a log result. 18 final class LogResult 19 { 20 private: 21 /// Unique token for the log. 22 string _logToken; 23 /// The log type. 24 LogType _logType; 25 /// The name of the application. 26 string _applicationName; 27 /// The ip address. 28 string _ipAddress; 29 /// The request method. 30 HttpMethod _requestMethod; 31 /// The request headers. 32 string _requestHeaders; 33 /// The request body. 34 string _requestBody; 35 /// The request url. 36 string _requestUrl; 37 /// The response headers. 38 string _responseHeaders; 39 /// The response body. 40 string _responseBody; 41 /// The response status code. 42 HttpStatus _responseStatusCode; 43 /// The message. 44 string _message; 45 /// The auth token. 46 string _authToken; 47 48 /** 49 * Creates a new log result. 50 * Params: 51 * logToken = The token of the log result. 52 * logType = The type of the log. 53 * applicationName = The name of the application. 54 * ipAddress = The ip address. 55 * requestMethod = The request method. 56 * requestHeaders = The request headers. 57 * requestBody = The request body. 58 * requestUrl = The request url. 59 * responseHeaders = The response headers. 60 * responseBody = The response body. 61 * responseStatusCode The response status code. 62 * message = The message. 63 */ 64 this 65 ( 66 string logToken, 67 LogType logType, 68 string applicationName, 69 string ipAddress, 70 HttpMethod requestMethod, 71 string requestHeaders, 72 string requestBody, 73 string requestUrl, 74 string responseHeaders, 75 string responseBody, 76 HttpStatus responseStatusCode, 77 string message, 78 string authToken 79 ) 80 { 81 _logToken = logToken; 82 _logType = logType; 83 _applicationName = applicationName; 84 _ipAddress = ipAddress; 85 _requestMethod = requestMethod; 86 _requestHeaders = requestHeaders; 87 _requestBody = requestBody; 88 _requestUrl = requestUrl; 89 _responseHeaders = responseHeaders; 90 _responseBody = responseBody; 91 _responseStatusCode = responseStatusCode; 92 _message = message; 93 _authToken = authToken; 94 } 95 96 public: 97 final: 98 @property 99 { 100 /// Gets the unique token for the log result. 101 string logToken() { return _logToken; } 102 103 /// Gets the type of the log. 104 LogType logType() { return _logType; } 105 106 /// Gets the name of the application. 107 string applicationName() { return _applicationName; } 108 109 /// Gets the ip address. 110 string ipAddress() { return _ipAddress; } 111 112 /// Gets the request method. 113 HttpMethod requestMethod() { return _requestMethod; } 114 115 /// Gets the request headers. 116 string requestHeaders() { return _requestHeaders; } 117 118 /// Gets the request body. 119 string requestBody() { return _requestBody; } 120 121 /// Gets the request url. 122 string requestUrl() { return _requestUrl; } 123 124 /// Gets the response headers. 125 string responseHeaders() { return _responseHeaders; } 126 127 /// Gets the response body. 128 string responseBody() { return _responseBody; } 129 130 /// Gets the response status code. 131 HttpStatus responseStatusCode() { return _responseStatusCode; } 132 133 /// Gets the message. 134 string message() { return _message; } 135 136 /// Gets the auth token. 137 string authToken() { return _authToken; } 138 } 139 140 /// Gets the log result as a loggable string, fit for ex. file-logs. 141 override string toString() 142 { 143 import std.datetime : Clock; 144 import std..string : format; 145 146 return `------------------- 147 --------- %s ---------- 148 ------------------- 149 Token: %s 150 LogType: %s 151 App: %s, 152 IPAddress: %s 153 Method: %s 154 Status: %s 155 ReqUrl: %s 156 AuthToken: %s 157 ReqHeaders: 158 ------------------- 159 %s 160 ------------------- 161 ReqBody: 162 ------------------- 163 %s 164 ------------------- 165 ResHeaders: 166 ------------------- 167 %s 168 ------------------- 169 ResBody: 170 ------------------- 171 %s 172 ------------------- 173 Message: 174 ------------------- 175 %s 176 ------------------- 177 -------------------`.format 178 ( 179 Clock.currTime().toString(), 180 logToken, logType, applicationName, 181 ipAddress, requestMethod, 182 responseStatusCode, requestUrl, 183 authToken, 184 requestHeaders, requestBody, 185 responseHeaders, responseBody, 186 message 187 ); 188 } 189 } 190 191 package(diamond) 192 { 193 /// Collection of loggers. 194 __gshared LogDelegate[][LogType] _loggers; 195 196 /** 197 * Executes a specific type of logger. 198 * Params: 199 * logType = The type of logger to execute. 200 * client = The client to log. 201 * message = The message associated with the log. 202 */ 203 void executeLog 204 ( 205 LogType logType, 206 HttpClient client, 207 lazy string message = null 208 ) 209 { 210 import diamond.core.webconfig; 211 212 auto loggers = _loggers.get(logType, null); 213 214 if (!loggers) 215 { 216 return; 217 } 218 219 import std.algorithm : canFind; 220 221 bool protectedPrivacy = !webConfig.disablePrivacyLogging; 222 bool protectedIP = protectedPrivacy || !client.privacy["__D_LOGGING_PROTECT_IP"].adminVisible; 223 bool protectedBody = protectedPrivacy || !client.privacy["__D_LOGGING_PROTECT_BODY"].adminVisible; 224 bool protectedHeaders = client.route && webConfig.disableHeaderLoggingRoutes && webConfig.disableHeaderLoggingRoutes.length && webConfig.disableHeaderLoggingRoutes.canFind(client.route.name); 225 226 import std..string : format, indexOf, lastIndexOf, toLower; 227 import vibe.stream.operations : readAllUTF8; 228 import diamond.core.webconfig; 229 import diamond.core.senc; 230 231 string protectIP(string ip) 232 { 233 if (!ip || !ip.length || ip.indexOf('.') == -1) 234 { 235 return ""; 236 } 237 238 auto newIP = ip[0 .. ip.indexOf('.')]; 239 auto last = ip.lastIndexOf('.'); 240 241 if (last > 0) 242 { 243 newIP ~= "..." ~ ip[ip.lastIndexOf('.') .. $]; 244 } 245 246 return newIP; 247 } 248 249 string requestHeaders = ""; 250 251 if (!protectedHeaders) 252 { 253 foreach (key,value; client.headers) 254 { 255 if (protectedIP && (key.toLower() == "x-forwarded-for" || key.toLower() == "x-real-ip" || key.toLower().canFind("ip") || key.toLower().canFind("address"))) 256 { 257 requestHeaders ~= "%s: %s\r\n".format(key, protectIP(value)); 258 } 259 else 260 { 261 requestHeaders ~= "%s: %s\r\n".format(key, value); 262 } 263 } 264 } 265 266 string responseHeaders = ""; 267 268 if (!protectedHeaders) 269 { 270 foreach (key,value; client.rawResponse.headers) 271 { 272 if (protectedIP && (key.toLower() == "x-forwarded-for" || key.toLower() == "x-real-ip" || key.toLower().canFind("ip") || key.toLower().canFind("address"))) 273 { 274 responseHeaders ~= "%s: %s\r\n".format(key, protectIP(value)); 275 } 276 else 277 { 278 responseHeaders ~= "%s: %s\r\n".format(key, value); 279 } 280 } 281 } 282 283 import std.uuid : randomUUID, sha1UUID; 284 import std.random : Xorshift192, unpredictableSeed; 285 286 Xorshift192 gen; 287 gen.seed(unpredictableSeed); 288 289 string logToken = 290 randomUUID(gen).toString() ~ "-" ~ sha1UUID(client.session.id).toString(); 291 292 auto statusCode = client.statusCode; 293 294 if (logType == LogType.error) 295 { 296 statusCode = HttpStatus.internalServerError; 297 } 298 else if (logType == LogType.notFound) 299 { 300 statusCode = HttpStatus.notFound; 301 } 302 else if (statusCode == HttpStatus.continue_) 303 { 304 statusCode = HttpStatus.ok; 305 } 306 307 auto logResult = new LogResult 308 ( 309 logToken, 310 logType, 311 webConfig.name, 312 protectedIP ? protectIP(client.ipAddress) : client.ipAddress, 313 client.method, 314 requestHeaders ? requestHeaders : "", 315 protectedBody ? "" : client.requestStream.readAllUTF8(), 316 client.fullUrl.toString(), 317 responseHeaders, 318 protectedBody ? "" : 319 ( 320 client.rawResponse.contentType.canFind("text") ? 321 cast(string)client.getBody() : SENC.encode(client.getBody()) 322 ), 323 statusCode, 324 message ? message : "", 325 client.cookies.hasAuthCookie() ? client.cookies.getAuthCookie() : "" 326 ); 327 328 foreach (logger; loggers) 329 { 330 logger(logResult); 331 } 332 } 333 } 334 335 /// Enumeration of log types. 336 enum LogType 337 { 338 /// An error logger. 339 error, 340 341 /// A not-found logger. 342 notFound, 343 344 /// A logger for pre-request handling. 345 before, 346 347 /// A logger for post-request handling. 348 after, 349 350 /// A logger for static files. 351 staticFile 352 } 353 354 /** 355 * Creates a logger. 356 * Params: 357 * logType = The type of the logger. 358 * logger = The logger handler. 359 */ 360 void log(LogType logType, void delegate(LogResult) logger) 361 { 362 _loggers[logType] ~= logger; 363 } 364 365 /** 366 * Creates a file logger. 367 * Params: 368 * logType = The type of the logger. 369 * file = The file append logs to. 370 * callback = An optional callback after the log has been written. 371 */ 372 void logToFile(LogType logType, string file, void delegate(LogResult) callback = null) 373 { 374 log(logType, 375 (result) 376 { 377 import std.file : append; 378 append(file, result.toString()); 379 380 if (callback !is null) 381 { 382 callback(result); 383 } 384 }); 385 } 386 387 /** 388 * Creates a database logger. 389 * The table must implement the following columns: 390 * logToken (VARCHAR) 391 * logType (ENUM ("error", "notFound", "after", "before", "staticFile")) 392 * applicationName (VARCHAR) 393 * authToken (VARCHAR) 394 * requestIPAddress (VARCHAR) 395 * requestMethod (VARCHAR) 396 * requestHeaders (TEXT) 397 * requestBody (TEXT) 398 * requestUrl (VARCHAR) 399 * responseHeaders (TEXT) 400 * responseBody (TEXT) 401 * responseStatusCode (INT) 402 * message (TEXT) 403 * timestamp (DATETIME) 404 * Params: 405 * logType = The type of the logger. 406 * table = The table to log entries to. 407 * callback = An optional callback after the log has been written. 408 * connectionString = A connection string to associate with the logging. If none is specified then it will use the default connection string. 409 */ 410 void logToDatabase(LogType logType, string table, void delegate(LogResult) callback = null, string connectionString = null) 411 { 412 log(logType, 413 (result) 414 { 415 import std..string : format; 416 417 auto sql = " 418 INSERT INTO `%s` 419 ( 420 `logToken`, 421 `logType`, 422 `applicationName`, 423 `authToken`, 424 `requestIPAddress`, `requestMethod`, `requestHeaders`, 425 `requestBody`, `requestUrl`, 426 `responseHeaders`, `responseBody`, `responseStatusCode`, 427 `message`, 428 `timestamp` 429 ) 430 VALUES 431 ( 432 @logToken, 433 @logType, 434 @applicationName, 435 @authToken, 436 @requestIPAddress, @requestMethod, @requestHeaders, 437 @requestBody, @requestUrl, 438 @responseHeaders, @responseBody, @responseStatusCode, 439 @message, 440 NOW() 441 )".format(table); 442 443 import std.conv : to; 444 import diamond.database; 445 446 auto params = getParams(); 447 params["logToken"] = result.logToken; 448 params["logType"] = to!string(result.logType); 449 params["applicationName"] = result.applicationName; 450 params["authToken"] = result.authToken; 451 params["requestIPAddress"] = result.ipAddress; 452 params["requestMethod"] = to!string(result.requestMethod); 453 params["requestHeaders"] = result.requestHeaders; 454 params["requestBody"] = result.requestBody; 455 params["requestUrl"] = result.requestUrl; 456 params["responseHeaders"] = result.responseHeaders; 457 params["responseBody"] = result.responseBody; 458 params["responseStatusCode"] = cast(int)result.responseStatusCode; 459 params["message"] = result.message; 460 461 MySql.mySqlAdapter.execute(sql, params, connectionString); 462 463 if (callback !is null) 464 { 465 callback(result); 466 } 467 }); 468 } 469 }