282df52731f78b74c12d678f51f3a43289a1aff9
[gnucomo.git] / src / gcm_input / message.cpp
1
2 /**************************************************************************
3 **  (c) Copyright 2002, Andromeda Technology & Automation
4 ** This is free software; you can redistribute it and/or modify it under the
5 ** terms of the GNU General Public License, see the file COPYING.
6 ***************************************************************************
7 ** MODULE INFORMATION *
8 ***********************
9 **      FILE NAME      : message.cpp
10 **      SYSTEM NAME    : Gnucomo - Gnu Computer Monitoring
11 **      VERSION NUMBER : $Revision: 1.4 $
12 **
13 **  DESCRIPTION      :  Implementation of the message handling classes
14 **
15 **  EXPORTED OBJECTS : 
16 **  LOCAL    OBJECTS : 
17 **  MODULES  USED    :
18 ***************************************************************************
19 **  ADMINISTRATIVE INFORMATION *
20 ********************************
21 **      ORIGINAL AUTHOR : Arjen Baart - arjen@andromeda.nl
22 **      CREATION DATE   : Sep 16, 2002
23 **      LAST UPDATE     : Nov 29, 2002
24 **      MODIFICATIONS   : 
25 **************************************************************************/
26
27 /*****************************
28    $Log: message.cpp,v $
29    Revision 1.4  2002-12-06 22:26:28  arjen
30    Set the value of log.processed to FALSE when inserting a
31    new log entry into the database
32    When a syslog entry arrives from last year, gcm_input subtracts one from the
33    year of arrival to create the year of the log entry.
34    Read output from "rpm -qa" and enter packages in the parameter table.
35
36    Revision 1.3  2002/11/09 08:04:27  arjen
37    Added a reference to the GPL
38
39    Revision 1.2  2002/11/04 10:13:36  arjen
40    Use proper namespace for iostream classes
41
42    Revision 1.1  2002/10/05 10:25:49  arjen
43    Creation of gcm_input and a first approach to a web interface
44
45 *****************************/
46
47 static const char *RCSID = "$Id: message.cpp,v 1.4 2002-12-06 22:26:28 arjen Exp $";
48
49 #include "message.h"
50
51 extern bool verbose;   /*  Defined in the main application */
52 extern bool testmode;
53
54 /*   Utility functions   */
55
56 String SQL_Escape(String s);
57
58 /*=========================================================================
59 **  NAME           : operator >>
60 **  SYNOPSIS       : bool operator >> (message_buffer &, String &)
61 **  PARAMETERS     : 
62 **  RETURN VALUE   : True if input was available.
63 **
64 **  DESCRIPTION    : Input operator. Read the next line from the message.
65 **
66 **  VARS USED      :
67 **  VARS CHANGED   :
68 **  FUNCTIONS USED :
69 **  SEE ALSO       :
70 **  LAST MODIFIED  : Nov 04, 2002
71 **=========================================================================
72 */
73
74 bool operator >> (message_buffer &b, String &s)
75 {
76    bool   input_ok = false;
77
78    if (b.next_line == b.buffer.end())
79    {
80       String   l;
81
82       if (*(b.input) >> l)
83       {
84          b.buffer.push_back(l);
85
86          //   next_line keeps pointing to the end.
87  
88          s = l;
89          input_ok = true;
90       }
91    }
92    else
93    {
94       s = *(b.next_line);
95       b.next_line++;
96       input_ok = true;
97    }
98    return input_ok;
99 }
100
101 client_message::client_message(std::istream *in, gnucomo_database db)
102 {
103    input.from(in);
104    database = db;
105
106    hostname = "";
107    mail_header   = false;
108    gpg_encrypted = false;
109    classification = UNKNOWN;
110    certainty      = 0.0;
111 }
112
113 static const String syslog_date_re("[[:alpha:]]{3} [ 123][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2}");
114 static const String mail_date_re("[[:alpha:]]{3}, [ 123]?[0-9] [[:alpha:]]{3} [0-9]{4} [0-9]{2}:[0-9]{2}:[0-9]{2} [+-][0-9]{4}");
115 static const String unix_date_re("[[:alpha:]]{3} [[:alpha:]]{3} [ 123][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2} [0-9]{4}");
116
117 static const regex re_syslog(syslog_date_re + " [a-z]+ [[:alpha:]]+.*:.+");
118 static const regex re_PGP("-----BEGIN PGP MESSAGE-----");
119 static const regex re_dump("^ *DUMP: Date of this level");
120 static const regex re_accesslog("(GET|POST) .+ HTTP");
121 static const regex re_errorlog("^\\[" + unix_date_re + "\\] \\[(error|notice)\\] .+");
122 static const regex re_rpm("[[:alnum:]+-]+-[0-9][[:alnum:].-]");
123
124 static const regex re_syslog_date("[[:alpha:]]{3} [ 123][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2}");
125 static const regex re_uxmail_from("^From - " + unix_date_re);
126 static const regex re_mail_From("^From:[[:blank:]]+");
127 static const regex re_mail_Date("^Date:[[:blank:]]+" + mail_date_re);
128 static const regex re_email_address("[[:alnum:]_.-]+@[[:alnum:]_.-]+");
129 static const regex re_email_user("[[:alnum:]_.-]+@");
130
131 /*=========================================================================
132 **  NAME           : classify
133 **  SYNOPSIS       : double classify(String host, date arriv_d, hour arriv_t, String serv)
134 **  PARAMETERS     : 
135 **  RETURN VALUE   : The certainty with which the message is classified.
136 **
137 **  DESCRIPTION    : 
138 **
139 **  VARS USED      :
140 **  VARS CHANGED   :
141 **  FUNCTIONS USED :
142 **  SEE ALSO       :
143 **  LAST MODIFIED  : Nov 16, 2002
144 **=========================================================================
145 */
146
147 double client_message::classify(String host, UTC arriv, String serv)
148 {
149    String    line;
150
151    hostname    = host;
152    arrival     = arriv;
153    service     = serv;
154
155    /*  First, check if the message has a mail header. */
156
157    if (input >> line && line == re_uxmail_from)
158    {
159       String    from_address;
160
161       mail_header = true;
162
163       /*  Scan ahead for the hostname and date of arrival.  */
164
165       while (input >> line && line != "")
166       {
167          if (line == re_mail_From)
168          {
169             from_address = line(re_email_address);
170             from_address(re_email_user) = "";            //  Remove the user part;
171             hostname = from_address;
172          }
173          if (line == re_mail_Date)
174          {
175             arrival = UTC(line(regex(mail_date_re)));
176          }
177       }
178    }
179    else
180    {
181       //  Push the first line back, we need to read it again.
182       --input;
183    }
184
185    /*
186     *  Now that we have the mail header out of the way, try to figure
187     *  out what the content of the message is.
188     */
189
190
191    while (input >> line && certainty < 0.9)
192    {
193       std::cout << "  testing: " << line << "\n";
194       if (line == re_syslog)
195       {
196          certainty = 1.0;
197          classification = SYSLOG;
198          if (verbose)
199          {
200             std::cout << "Syslog detected.\n";
201          }
202       }
203       else if (line == re_PGP)
204       {
205          certainty = 1.0;
206          gpg_encrypted = true;
207          std::cerr << "The message is PGP/GnuPG encrypted.\n";
208       }
209       else if (line == re_dump)
210       {
211           certainty = 1.0;
212           if (verbose)
213           {
214              std::cout << "DUMP output detected.\n";
215           }
216       }
217       else if (line == re_accesslog)
218       {
219           certainty = 1.0;
220           classification = ACCESSLOG;
221           service = "httpd";
222           if (verbose)
223           {
224              std::cout << "HTTP access log detected.\n";
225           }
226       }
227       else if (line == re_errorlog)
228       {
229           certainty = 1.0;
230           classification = ERRORLOG;
231           service = "httpd";
232           if (verbose)
233           {
234              std::cout << "HTTP error log detected.\n";
235           }
236       }
237       else if (line == re_rpm)
238       {
239           certainty = 1.0;
240           classification = RPMLIST;
241           service = "";
242           if (verbose)
243           {
244              std::cout << "RPM package list detected.\n";
245           }
246       }
247    }
248    input.rewind();
249
250    if (hostname == "")
251    {
252       std::cerr <<  "Can not determine the hostname where the message came from.\n";
253       certainty = 0.0;
254    }
255    else if (!arrival.proper())
256    {
257       std::cerr << "Arrival time is not knwon.\n";
258       certainty = 0.0;
259    }
260    else
261    {
262       certainty = 1.0;
263    }
264
265    return certainty;
266 }
267
268 /*=========================================================================
269 **  NAME           : enter
270 **  SYNOPSIS       : int enter()
271 **  PARAMETERS     : 
272 **  RETURN VALUE   : The number of lines successfully parsed from the input
273 **
274 **  DESCRIPTION    : 
275 **
276 **  VARS USED      :
277 **  VARS CHANGED   :
278 **  FUNCTIONS USED :
279 **  SEE ALSO       :
280 **  LAST MODIFIED  : Nov 29, 2002
281 **=========================================================================
282 */
283
284 int client_message::enter()
285 {
286    long   nr_lines = 0;
287    String line;
288
289    /*  Double-check the classification of the message */
290
291    if (classification == UNKNOWN || certainty < 0.9 || gpg_encrypted)
292    {
293       return 0;
294    }
295
296    if (mail_header)
297    {
298       //  Skip the mail header.
299  
300       while (input >> line && line != "");
301    }
302
303    /*  Try to find the host in the database */
304
305    String objectid;
306
307    objectid = database.find_host(hostname);
308    if (objectid == "")
309    {
310       std::cerr << "Please define the host " << hostname << " in the database.\n";
311       return 0;
312    }
313    if (verbose)
314    {
315       std::cout << "Object id for " << hostname << " is " << objectid << "\n";
316    }
317
318    /*  Scan the input line by line, entring records into the database */
319
320    String rest;   //  Rest of the line to be parsed
321
322    while (input >> line)
323    {
324       if (verbose)
325       {
326          std::cout << line << "\n";
327       }
328
329
330       /*  Check each line if it contains valid information */
331
332       const regex *check;
333
334       switch (classification)
335       {
336       case SYSLOG:
337             check = &re_syslog;
338             break;
339       case ACCESSLOG:
340             check = &re_accesslog;
341             break;
342       case ERRORLOG:
343             check = &re_errorlog;
344             break;
345       case RPMLIST:
346             check = &re_rpm;
347             break;
348       }
349
350       if (line == *check)
351       {
352          date   log_date;
353          hour   log_time;
354          int    i;
355
356          String insertion("insert into log (objectid, servicecode,"
357                            " object_timestamp, timestamp, rawdata, processed) values (");
358          String datestring;
359
360          switch (classification)
361          {
362          case SYSLOG:
363             log_date = line;
364             log_time = line;
365             if (log_date.Year() < 0 || log_date.Year() > 2500)
366             {
367                //  The year is not in the log file. Assume the year of arrival,
368                //  unless this puts the log entry at a later date than the arrival date.
369                //  This happens e.g. when a log entry from December arrives in Januari.
370
371                log_date = date(log_date.Day(), log_date.Month(), date(arrival).Year());
372                if (log_date > date(arrival))
373                {
374                   log_date = date(log_date.Day(), log_date.Month(), date(arrival).Year() - 1);
375                }
376             }
377
378             if (verbose)
379             {
380                std::cout << "   Log timestamp  = " << log_date << " " << log_time << "\n";
381             }
382             rest = line << 16;
383             i = rest.index(' ');
384             if (rest(0,i) == hostname(0,i))
385             {
386                rest <<= i + 1;
387                if (verbose)
388                {
389                   std::cout << "   Hostname matches.\n";
390                   std::cout << "   rest = " << rest << "\n";
391                }
392                for (i = 0; isalpha(rest[i]) && i < ~rest; i++);
393                if (verbose)
394                {
395                   std::cout << "   Service name = " << rest(0,i) << "\n";
396                }
397
398                /*   Insert a new record into the log table   */
399
400                insertion += "'" + objectid + "',";
401                insertion += "'" + rest(0,i) + "',";
402                insertion += "'" + log_date.format() + " " + log_time.format() + "',";
403                insertion += "'" + arrival.format() + "',";
404                insertion += "'" + SQL_Escape(line) + "',FALSE";
405                insertion += ")";
406             
407                if (testmode)
408                {
409                   std::cout << insertion << "\n";
410                }
411                else
412                {
413                   database.Query(insertion);
414                }
415
416                if (verbose)
417                {
418                   std::cout << "\n\n";
419                }
420
421                nr_lines++;
422             }
423             else
424             {
425                std::cerr << "   Hostname " << rest(0,i) << " does not match.\n";
426             }
427             break;
428
429          case ACCESSLOG:
430             datestring = line(regex("\\[.+\\]"));
431             datestring <<= 1;
432             datestring >>= 1;
433             datestring[datestring.index(':')] = ' ';
434             log_date = datestring;
435             log_time = datestring;
436             if (verbose)
437             {
438                std::cout << "   Log timestamp  = " << log_date << " " << log_time << "\n";
439             }
440             insertion += "'" + objectid + "',";
441             insertion += "'" + service + "',";
442             insertion += "'" + log_date.format() + " " + log_time.format() + "',";
443             insertion += "'" + arrival.format() + "',";
444             insertion += "'" + SQL_Escape(line) + "',FALSE";
445             insertion += ")";
446             
447             if (testmode)
448             {
449                std::cout << insertion << "\n";
450             }
451             else
452             {
453                database.Query(insertion);
454             }
455
456             if (verbose)
457             {
458                std::cout << "\n\n";
459             }
460
461             nr_lines++;
462             break;
463
464          case ERRORLOG:
465             datestring = line(regex("\\[.+\\]"));
466             datestring <<= 1;
467             datestring >>= 1;
468             log_date = datestring;
469             log_time = datestring;
470             if (verbose)
471             {
472                std::cout << "   Log timestamp  = " << log_date << " " << log_time << "\n";
473             }
474             insertion += "'" + objectid + "',";
475             insertion += "'" + service + "',";
476             insertion += "'" + log_date.format() + " " + log_time.format() + "',";
477             insertion += "'" + arrival.format() + "',";
478             insertion += "'" + SQL_Escape(line) + "',FALSE";
479             insertion += ")";
480             
481             if (testmode)
482             {
483                std::cout << insertion << "\n";
484             }
485             else
486             {
487                database.Query(insertion);
488             }
489
490             if (verbose)
491             {
492                std::cout << "\n\n";
493             }
494
495             nr_lines++;
496             break;
497
498          case RPMLIST:
499             //  Scan a list of packages and versions from "rpm -a".
500             //  A similar listing can be created on IRIX 6.5 by using the
501             //  command "showprods -3 -n|awk '{printf "%s-%s\n",$2,$3}'|grep -v ^[-=] \
502             //            |grep -v Version-Description".
503             //
504             //  We have to separate the package name and the version.
505             //  The separation is marked by a '-', followed by a digit.
506  
507             i = line.index('-');
508             while (!(line[i] == '-' && isdigit(line[i + 1])))
509             {
510                i++;
511             }
512             String package(line(0,i));
513             String version(line(i+1, ~line));
514             String paramid;
515
516             if (verbose)
517             {
518                std::cout << "Package is " << package;
519                std::cout << ", version is " << version << "\n";
520             }
521
522             //  Construct a qry to check the package's existance
523
524             String qry = "select paramid from parameter where objectid='";
525             qry += objectid + "' and class='package' and name='";
526             qry += package + "'";
527
528             if (database.Query(qry) == 1)
529             {
530                paramid = database.Field(0, "paramid");
531                qry = "select value from property where paramid='";
532                qry += paramid + "' and name='version'";
533                if (database.Query(qry) == 0)
534                {
535                   std::cerr << "Database corruption: Package " << package;
536                   std::cerr << " does not have a 'version' property.\n";
537                }
538                else if (database.Field(0, "value") != version)
539                {
540                   if (verbose)
541                   {
542                      std::cout << "  Parameter " << package << " has different version\n";
543                   }
544                }
545                else
546                {
547                   if (verbose)
548                   {
549                      std::cout << "   Parameter " << package << " has not changed.\n";
550                   }
551                }
552             }
553             else
554             {
555                String insert_h;
556
557                if (verbose)
558                {
559                   std::cout << "  Parameter " << package << " does not exist.\n";
560                }
561                //  Create a new package parameter, including version property and history record
562
563                insertion = "insert into parameter (objectid, name, class, description) values ('";
564                insertion += objectid + "', '" + package + "', 'package', 'RPM package " + package + "')";
565                if (testmode)
566                {
567                   paramid = "0";
568                   std::cout << insertion << "\n";
569                }
570                else
571                {
572                   database.Query(insertion);
573                   qry = "select paramid from parameter where objectid='";
574                   qry += objectid + "' and class='package' and name='";
575                   qry += package + "'";
576                   database.Query(qry);
577                   paramid = database.Field(0, "paramid");
578                }
579
580                insertion = "insert into property (paramid, name, value, type) values ('";
581                insertion += paramid + "', 'version', '";
582                insertion += version + "', 'STATIC')";
583                insert_h = "insert into history (paramid, modified, change_nature, changed_property, new_value)";
584                insert_h += " values ('";
585                insert_h += paramid + "', '" + arrival.format() + "', 'CREATED', 'version', '";
586                insert_h += version + "')";
587
588                if (testmode)
589                {
590                   std::cout << insertion << "\n" << insert_h << "\n";
591                }
592                else
593                {
594                   database.Query(insertion);
595                   database.Query(insert_h);
596                }
597             }
598
599             if (verbose)
600             {
601                std::cout << "\n";
602             }
603
604             nr_lines++;
605             break;
606
607          }
608       }
609       else
610       {
611          std::cerr << "gcm_input WARNING: Not a valid line: " << line << "\n";
612       }
613    }
614
615    if (verbose)
616    {
617       std::cout << nr_lines << " lines parsed from the log file.\n";
618    }
619    return nr_lines;
620 }
621
622 /*=========================================================================
623 **  NAME           : SQL_Escape
624 **  SYNOPSIS       : String SQL_Escape(String)
625 **  PARAMETERS     : 
626 **  RETURN VALUE   : 
627 **
628 **  DESCRIPTION    : Insert backslashes before single quotes.
629 **
630 **  VARS USED      :
631 **  VARS CHANGED   :
632 **  FUNCTIONS USED :
633 **  SEE ALSO       :
634 **  LAST MODIFIED  : 
635 **=========================================================================
636 */
637
638 String SQL_Escape(String s)
639 {
640    int i;
641
642    for (i = 0; i < ~s; i++)
643    {
644       if (s[i] == '\'')
645       {
646          s(i,0) = "\\";
647          i++;
648       }
649    }
650
651    return s;
652 }