96214b5861e0b3ffbee4fb9f02fd2cfcdd9d603e
[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.7 $
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     : Feb 19, 2003
24 **      MODIFICATIONS   : 
25 **************************************************************************/
26
27 /*****************************
28    $Log: message.cpp,v $
29    Revision 1.7  2003-02-21 08:08:05  arjen
30    Gcm_input also detects packages that are removed from the system.
31    Determining the version number of a package in a RPM
32    list is improved. Only the last one or two parts of the string that
33    begin with a '-' and a number are considered the version.
34
35    Revision 1.6  2003/02/05 09:37:51  arjen
36    Create notifications when a new package is discovered
37    in a 'rpm -qa' list or when the version of a package is changed.
38
39    Revision 1.4  2002/12/06 22:26:28  arjen
40    Set the value of log.processed to FALSE when inserting a
41    new log entry into the database
42    When a syslog entry arrives from last year, gcm_input subtracts one from the
43    year of arrival to create the year of the log entry.
44    Read output from "rpm -qa" and enter packages in the parameter table.
45
46    Revision 1.3  2002/11/09 08:04:27  arjen
47    Added a reference to the GPL
48
49    Revision 1.2  2002/11/04 10:13:36  arjen
50    Use proper namespace for iostream classes
51
52    Revision 1.1  2002/10/05 10:25:49  arjen
53    Creation of gcm_input and a first approach to a web interface
54
55 *****************************/
56
57 static const char *RCSID = "$Id: message.cpp,v 1.7 2003-02-21 08:08:05 arjen Exp $";
58
59 #include <algorithm>
60 #include "message.h"
61
62 extern bool verbose;   /*  Defined in the main application */
63 extern bool testmode;
64
65 /*   Utility functions   */
66
67 String SQL_Escape(String s);
68
69 /*=========================================================================
70 **  NAME           : operator >>
71 **  SYNOPSIS       : bool operator >> (message_buffer &, String &)
72 **  PARAMETERS     : 
73 **  RETURN VALUE   : True if input was available.
74 **
75 **  DESCRIPTION    : Input operator. Read the next line from the message.
76 **
77 **  VARS USED      :
78 **  VARS CHANGED   :
79 **  FUNCTIONS USED :
80 **  SEE ALSO       :
81 **  LAST MODIFIED  : Nov 04, 2002
82 **=========================================================================
83 */
84
85 bool operator >> (message_buffer &b, String &s)
86 {
87    bool   input_ok = false;
88
89    if (b.next_line == b.buffer.end())
90    {
91       String   l;
92
93       if (*(b.input) >> l)
94       {
95          b.buffer.push_back(l);
96
97          //   next_line keeps pointing to the end.
98
99          s = l;
100          input_ok = true;
101       }
102    }
103    else
104    {
105       s = *(b.next_line);
106       b.next_line++;
107       input_ok = true;
108    }
109    return input_ok;
110 }
111
112 client_message::client_message(std::istream *in, gnucomo_database db)
113 {
114    input.from(in);
115    database = db;
116
117    hostname = "";
118    mail_header   = false;
119    gpg_encrypted = false;
120    classification = UNKNOWN;
121    certainty      = 0.0;
122 }
123
124 static const String syslog_date_re("[[:alpha:]]{3} [ 123][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2}");
125 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}");
126 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}");
127
128 static const regex re_syslog(syslog_date_re + " [[:alnum:]]+ [[:alpha:]]+.*:.+");
129 static const regex re_PGP("-----BEGIN PGP MESSAGE-----");
130 static const regex re_dump("^ *DUMP: Date of this level");
131 static const regex re_accesslog("(GET|POST) .+ HTTP");
132 static const regex re_errorlog("^\\[" + unix_date_re + "\\] \\[(error|notice)\\] .+");
133 static const regex re_rpm("[[:alnum:]+-]+-[0-9][[:alnum:].-]");
134
135 static const regex re_syslog_date("[[:alpha:]]{3} [ 123][0-9] [0-9]{2}:[0-9]{2}:[0-9]{2}");
136 static const regex re_uxmail_from("^From - " + unix_date_re);
137 static const regex re_mail_From("^From:[[:blank:]]+");
138 static const regex re_mail_Date("^Date:[[:blank:]]+" + mail_date_re);
139 static const regex re_email_address("[[:alnum:]_.-]+@[[:alnum:]_.-]+");
140 static const regex re_email_user("[[:alnum:]_.-]+@");
141
142 /*=========================================================================
143 **  NAME           : classify
144 **  SYNOPSIS       : double classify(String host, date arriv_d, hour arriv_t, String serv)
145 **  PARAMETERS     : 
146 **  RETURN VALUE   : The certainty with which the message is classified.
147 **
148 **  DESCRIPTION    : 
149 **
150 **  VARS USED      :
151 **  VARS CHANGED   :
152 **  FUNCTIONS USED :
153 **  SEE ALSO       :
154 **  LAST MODIFIED  : Nov 16, 2002
155 **=========================================================================
156 */
157
158 double client_message::classify(String host, UTC arriv, String serv)
159 {
160    String    line;
161
162    hostname    = host;
163    arrival     = arriv;
164    service     = serv;
165
166    /*  First, check if the message has a mail header. */
167
168    if (input >> line && line == re_uxmail_from)
169    {
170       String    from_address;
171
172       mail_header = true;
173
174       /*  Scan ahead for the hostname and date of arrival.  */
175
176       while (input >> line && line != "")
177       {
178          if (line == re_mail_From)
179          {
180             from_address = line(re_email_address);
181             from_address(re_email_user) = "";            //  Remove the user part;
182             hostname = from_address;
183          }
184          if (line == re_mail_Date)
185          {
186             arrival = UTC(line(regex(mail_date_re)));
187          }
188       }
189    }
190    else
191    {
192       //  Push the first line back, we need to read it again.
193       --input;
194    }
195
196    /*
197     *  Now that we have the mail header out of the way, try to figure
198     *  out what the content of the message is.
199     */
200
201
202    while (input >> line && certainty < 0.9)
203    {
204       std::cout << "  testing: " << line << "\n";
205       if (line == re_syslog)
206       {
207          certainty = 1.0;
208          classification = SYSLOG;
209          if (verbose)
210          {
211             std::cout << "Syslog detected.\n";
212          }
213       }
214       else if (line == re_PGP)
215       {
216          certainty = 1.0;
217          gpg_encrypted = true;
218          std::cerr << "The message is PGP/GnuPG encrypted.\n";
219       }
220       else if (line == re_dump)
221       {
222           certainty = 1.0;
223           if (verbose)
224           {
225              std::cout << "DUMP output detected.\n";
226           }
227       }
228       else if (line == re_accesslog)
229       {
230           certainty = 1.0;
231           classification = ACCESSLOG;
232           service = "httpd";
233           if (verbose)
234           {
235              std::cout << "HTTP access log detected.\n";
236           }
237       }
238       else if (line == re_errorlog)
239       {
240           certainty = 1.0;
241           classification = ERRORLOG;
242           service = "httpd";
243           if (verbose)
244           {
245              std::cout << "HTTP error log detected.\n";
246           }
247       }
248       else if (line == re_rpm)
249       {
250           certainty = 1.0;
251           classification = RPMLIST;
252           service = "";
253           if (verbose)
254           {
255              std::cout << "RPM package list detected.\n";
256           }
257       }
258    }
259    input.rewind();
260
261    if (hostname == "")
262    {
263       std::cerr <<  "Can not determine the hostname where the message came from.\n";
264       certainty = 0.0;
265    }
266    else if (!arrival.proper())
267    {
268       std::cerr << "Arrival time is not knwon.\n";
269       certainty = 0.0;
270    }
271    else
272    {
273       certainty = 1.0;
274    }
275
276    return certainty;
277 }
278
279 /*=========================================================================
280 **  NAME           : enter
281 **  SYNOPSIS       : int enter()
282 **  PARAMETERS     : 
283 **  RETURN VALUE   : The number of lines successfully parsed from the input
284 **
285 **  DESCRIPTION    : 
286 **
287 **  VARS USED      :
288 **  VARS CHANGED   :
289 **  FUNCTIONS USED :
290 **  SEE ALSO       :
291 **  LAST MODIFIED  : Feb 19, 2003
292 **=========================================================================
293 */
294
295 int client_message::enter()
296 {
297    long   nr_lines = 0;
298    String line;
299    String qry;
300
301    String change_notification("");
302    String create_notification("");
303    bool initial_entry = false;
304
305    std::list<String>  packages;
306
307
308    /*  Double-check the classification of the message */
309
310    if (classification == UNKNOWN || certainty < 0.9 || gpg_encrypted)
311    {
312       return 0;
313    }
314
315    if (mail_header)
316    {
317       //  Skip the mail header.
318
319       while (input >> line && line != "");
320    }
321
322    /*  Try to find the host in the database */
323
324    String objectid;
325
326    objectid = database.find_host(hostname);
327    if (objectid == "")
328    {
329       std::cerr << "Please define the host " << hostname << " in the database.\n";
330       return 0;
331    }
332    if (verbose)
333    {
334       std::cout << "Object id for " << hostname << " is " << objectid << "\n";
335    }
336
337    if (classification == RPMLIST)
338    {
339
340       int n_packages;
341
342       /*   Read all packages, so we will know which ones are  */
343       /*   missing at the end.                                */
344
345       qry = "select name from parameter where objectid='";
346       qry += objectid + "' and class='package'";
347       n_packages = database.Query(qry);
348       initial_entry = n_packages == 0;
349
350       std::cout << n_packages << " packages in database.\n";
351       for (int t = 0; t < n_packages; t++)
352       {
353          packages.push_back(database.Field(t, "name"));
354       }
355       std::cout << "Package list built: " << packages.size() << ".\n";
356    }
357
358    /*  Scan the input line by line, entring records into the database */
359
360    String rest;   //  Rest of the line to be parsed
361
362    while (input >> line)
363    {
364       if (verbose)
365       {
366          std::cout << line << "\n";
367       }
368
369
370       /*  Check each line if it contains valid information */
371
372       const regex *check;
373
374       switch (classification)
375       {
376       case SYSLOG:
377             check = &re_syslog;
378             break;
379       case ACCESSLOG:
380             check = &re_accesslog;
381             break;
382       case ERRORLOG:
383             check = &re_errorlog;
384             break;
385       case RPMLIST:
386             check = &re_rpm;
387             break;
388       }
389
390       if (line == *check)
391       {
392          date   log_date;
393          hour   log_time;
394          int    i;
395
396          String insertion("insert into log (objectid, servicecode,"
397                            " object_timestamp, timestamp, rawdata, processed) values (");
398          String datestring;
399
400          switch (classification)
401          {
402          case SYSLOG:
403             log_date = line;
404             log_time = line;
405             if (log_date.Year() < 0 || log_date.Year() > 2500)
406             {
407                //  The year is not in the log file. Assume the year of arrival,
408                //  unless this puts the log entry at a later date than the arrival date.
409                //  This happens e.g. when a log entry from December arrives in Januari.
410
411                log_date = date(log_date.Day(), log_date.Month(), date(arrival).Year());
412                if (log_date > date(arrival))
413                {
414                   log_date = date(log_date.Day(), log_date.Month(), date(arrival).Year() - 1);
415                }
416             }
417
418             if (verbose)
419             {
420                std::cout << "   Log timestamp  = " << log_date << " " << log_time << "\n";
421             }
422             rest = line << 16;
423             i = rest.index(' ');
424             if (rest(0,i) == hostname(0,i))
425             {
426                rest <<= i + 1;
427                if (verbose)
428                {
429                   std::cout << "   Hostname matches.\n";
430                   std::cout << "   rest = " << rest << "\n";
431                }
432                for (i = 0; isalpha(rest[i]) && i < ~rest; i++);
433                if (verbose)
434                {
435                   std::cout << "   Service name = " << rest(0,i) << "\n";
436                }
437
438                /*   Insert a new record into the log table   */
439
440                insertion += "'" + objectid + "',";
441                insertion += "'" + rest(0,i) + "',";
442                insertion += "'" + log_date.format("%Y-%m-%d") + " " + log_time.format() + "',";
443                insertion += "'" + arrival.format("%Y-%m-%d %T") + "',";
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             }
463             else
464             {
465                std::cerr << "   Hostname " << rest(0,i) << " does not match.\n";
466             }
467             break;
468
469          case ACCESSLOG:
470             datestring = line(regex("\\[.+\\]"));
471             datestring <<= 1;
472             datestring >>= 1;
473             datestring[datestring.index(':')] = ' ';
474             log_date = datestring;
475             log_time = datestring;
476             if (verbose)
477             {
478                std::cout << "   Log timestamp  = " << log_date << " " << log_time << "\n";
479             }
480             insertion += "'" + objectid + "',";
481             insertion += "'" + service + "',";
482             insertion += "'" + log_date.format("%Y-%m-%d") + " " + log_time.format() + "',";
483             insertion += "'" + arrival.format("%Y-%m-%d %T") + "',";
484             insertion += "'" + SQL_Escape(line) + "',FALSE";
485             insertion += ")";
486
487             if (testmode)
488             {
489                std::cout << insertion << "\n";
490             }
491             else
492             {
493                database.Query(insertion);
494             }
495
496             if (verbose)
497             {
498                std::cout << "\n\n";
499             }
500
501             nr_lines++;
502             break;
503
504          case ERRORLOG:
505             datestring = line(regex("\\[.+\\]"));
506             datestring <<= 1;
507             datestring >>= 1;
508             log_date = datestring;
509             log_time = datestring;
510             if (verbose)
511             {
512                std::cout << "   Log timestamp  = " << log_date << " " << log_time << "\n";
513             }
514             insertion += "'" + objectid + "',";
515             insertion += "'" + service + "',";
516             insertion += "'" + log_date.format("%Y-%m-%d") + " " + log_time.format() + "',";
517             insertion += "'" + arrival.format("%Y-%m-%d %T") + "',";
518             insertion += "'" + SQL_Escape(line) + "',FALSE";
519             insertion += ")";
520
521             if (testmode)
522             {
523                std::cout << insertion << "\n";
524             }
525             else
526             {
527                database.Query(insertion);
528             }
529
530             if (verbose)
531             {
532                std::cout << "\n\n";
533             }
534
535             nr_lines++;
536             break;
537
538          case RPMLIST:
539             //  Scan a list of packages and versions from "rpm -a".
540             //  A similar listing can be created on IRIX 6.5 by using the
541             //  command "showprods -3 -n|awk '{printf "%s-%s\n",$2,$3}'|grep -v '^[-=]' \
542             //            |grep -v Version-Description".
543             //
544             //  We have to separate the package name and the version.
545             //  The separation is marked by a '-', followed by a digit.
546             //  However, there may be other sequences of '-'digit in the package name,
547             //  do we have to scan ahead until there is at most one such sequence
548             //  left in the version string. The '-'digit seqeunce inside the
549             //  version usually separates the version and the release number.
550
551             int  version_start, next_version_start;
552
553             i = line.index('-');
554             version_start = i;
555             next_version_start = i;
556
557             while (i < ~line - 1)
558             {
559                while (i < ~line - 1 && !(line[i] == '-' && isdigit(line[i + 1])))
560                {
561                   i++;
562                }
563                if (i < ~line - 1)
564                {
565                   version_start = next_version_start;
566                   next_version_start = i;
567                }
568                i++;
569             }
570             
571             String package(line(0,version_start));
572             String version(line(version_start + 1, ~line));
573             String paramid;
574             String remark;
575             String insert_h;
576
577             if (verbose)
578             {
579                std::cout << "Package is " << package;
580                std::cout << ", version is " << version << "\n";
581             }
582
583             //  Construct a qry to check the package's existance
584
585             qry = "select paramid from parameter where objectid='";
586             qry += objectid + "' and class='package' and name='";
587             qry += package + "'";
588
589             if (database.Query(qry) == 1)
590             {
591                std::list<String>::iterator  lp;
592
593                lp = find(packages.begin(), packages.end(), package);
594                if (lp != packages.end())
595                {
596                   packages.erase(lp);
597                }
598                else
599                {
600                   std::cerr <<  "Could NOT find " << package << " in list.\n";
601                }
602
603                paramid = database.Field(0, "paramid");
604                qry = "select value from property where paramid='";
605                qry += paramid + "' and name='version'";
606                if (database.Query(qry) == 0)
607                {
608                   std::cerr << "Database corruption: Package " << package;
609                   std::cerr << " does not have a 'version' property.\n";
610                }
611                else if (database.Field(0, "value") != version)
612                {
613                   if (verbose)
614                   {
615                      std::cout << "  Parameter " << package << " has different version\n";
616                   }
617                   insertion = "update property set value='";
618                   insertion += version + "' where paramid='";
619                   insertion += paramid + "' and name='version'";
620
621                   insert_h = "insert into history (paramid, modified, change_nature, changed_property, new_value)";
622                   insert_h += " values ('";
623                   insert_h += paramid + "', '" + arrival.format("%Y-%m-%d %T") + "', 'MODIFIED', 'version', '";
624                   insert_h += version + "')";
625
626                   database.Query(insertion);
627                   database.Query(insert_h);
628
629                   if (change_notification == "")
630                   {
631                      remark = "Gnucomo detected a different version for package parameter(s) ";
632                      change_notification = database.new_notification(objectid, "property modified", remark);
633                   }
634
635                   if (change_notification != "")
636                   {
637                      insertion = "insert into parameter_notification (notificationid, paramid) values ('";
638                      insertion += change_notification + "', '";
639                      insertion += paramid + "')";
640
641                      database.Query(insertion);
642                   }
643                   else
644                   {
645                      std::cerr << "gcm_input ERROR: Cannot create 'property modified' notification.\n";
646                   }
647                }
648                else
649                {
650                   if (verbose)
651                   {
652                      std::cout << "   Parameter " << package << " has not changed.\n";
653                   }
654                }
655             }
656             else
657             {
658
659                if (verbose)
660                {
661                   std::cout << "  Parameter " << package << " does not exist.\n";
662                }
663                //  Create a new package parameter, including version property and history record
664
665                insertion = "insert into parameter (objectid, name, class, description) values ('";
666                insertion += objectid + "', '" + package + "', 'package', 'RPM package " + package + "')";
667                if (testmode)
668                {
669                   paramid = "0";
670                   std::cout << insertion << "\n";
671                }
672                else
673                {
674                   database.Query(insertion);
675                   qry = "select paramid from parameter where objectid='";
676                   qry += objectid + "' and class='package' and name='";
677                   qry += package + "'";
678                   database.Query(qry);
679                   paramid = database.Field(0, "paramid");
680                }
681
682                insertion = "insert into property (paramid, name, value, type) values ('";
683                insertion += paramid + "', 'version', '";
684                insertion += version + "', 'STATIC')";
685                insert_h = "insert into history (paramid, modified, change_nature, changed_property, new_value)";
686                insert_h += " values ('";
687                insert_h += paramid + "', '" + arrival.format("%Y-%m-%d %T") + "', 'CREATED', 'version', '";
688                insert_h += version + "')";
689
690                if (testmode)
691                {
692                   std::cout << insertion << "\n" << insert_h << "\n";
693                }
694                else
695                {
696                   database.Query(insertion);
697                   database.Query(insert_h);
698                   if (!initial_entry)
699                   {
700                      if (create_notification == "")
701                      {
702                         remark = "Gnucomo detected new parameter(s) of class package";
703                         create_notification = database.new_notification(objectid, "parameter created", remark);
704                      }
705                      if (create_notification != "")
706                      {
707                         insertion = "insert into parameter_notification (notificationid, paramid) values ('";
708                         insertion += create_notification + "', '";
709                         insertion += paramid + "')";
710
711                         database.Query(insertion);
712                      }
713                      else
714                      {
715                         std::cerr << "gcm_input ERROR: Cannot create 'parameter created' notification.\n";
716                      }
717                   }
718                }
719             }
720
721             if (verbose)
722             {
723                std::cout << "\n";
724             }
725
726             nr_lines++;
727             break;
728
729          }
730       }
731       else
732       {
733          std::cerr << "gcm_input WARNING: Not a valid line: " << line << "\n";
734       }
735    }
736
737    if (classification == RPMLIST)
738    {
739       std::list<String>::iterator  lp;
740       String     remove_notification("");
741
742       /*
743        *     If there are any packages left in the list, they seem to have
744        *     disappeared from the system.
745        */
746
747       for (lp = packages.begin(); lp != packages.end(); lp++)
748       {
749          String paramid;
750          String remark;
751          String insert;
752
753          //  Construct a qry to check the package's existance
754
755          qry = "select paramid from parameter where objectid='";
756          qry += objectid + "' and class='package' and name='";
757          qry += *lp + "'";
758
759          if (database.Query(qry) == 1)
760          {
761             paramid = database.Field(0, "paramid");
762             qry ="select change_nature from history where paramid='";
763             qry += paramid + "' order by modified desc";
764             if (database.Query(qry) <= 0)
765             {
766                std::cerr << "Database ERROR: no history record for parameter " << *lp << ".\n";
767             }
768             else if (database.Field(0, "change_nature") != "REMOVED")
769             {
770                if (verbose)
771                {
772                  std::cout << "Removing parameter " << *lp << ".\n";
773                }
774
775                insert = "insert into history (paramid, modified, change_nature)";
776                insert += " values ('";
777                insert += paramid + "', '" + arrival.format("%Y-%m-%d %T") + "', 'REMOVED')";
778
779                database.Query(insert);
780
781                if (remove_notification == "")
782                {
783                   remark = "Gnucomo detected that package(s) have disappeared ";
784                   remove_notification = database.new_notification(objectid, "parameter removed", remark);
785                }
786
787                if (remove_notification != "")
788                {
789                   insert = "insert into parameter_notification (notificationid, paramid) values ('";
790                   insert += remove_notification + "', '";
791                   insert += paramid + "')";
792
793                   database.Query(insert);
794                }
795                else
796                {
797                   std::cerr << "gcm_input ERROR: Cannot create 'parameter removed' notification.\n";
798                }
799             }
800          }
801       }
802    }
803
804    if (verbose)
805    {
806       std::cout << nr_lines << " lines parsed from the log file.\n";
807    }
808    return nr_lines;
809 }
810
811 /*=========================================================================
812 **  NAME           : SQL_Escape
813 **  SYNOPSIS       : String SQL_Escape(String)
814 **  PARAMETERS     : 
815 **  RETURN VALUE   : 
816 **
817 **  DESCRIPTION    : Insert backslashes before single quotes.
818 **
819 **  VARS USED      :
820 **  VARS CHANGED   :
821 **  FUNCTIONS USED :
822 **  SEE ALSO       :
823 **  LAST MODIFIED  : 
824 **=========================================================================
825 */
826
827 String SQL_Escape(String s)
828 {
829    int i;
830
831    for (i = 0; i < ~s; i++)
832    {
833       if (s[i] == '\'')
834       {
835          s(i,0) = "\\";
836          i++;
837       }
838    }
839
840    return s;
841 }