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 }