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