//=-- AfIISAccessFormat.cpp - Plugin to parse Microsoft IIS log files ---------=
//
//  This class defines an AccessFormatPlugin that understands the log format of
//  the Microsoft IIS log format.
//
//=----------------------------------------------------------------------------=
//  This file is copyright (c) 2000 Chris Lattner
//=----------------------------------------------------------------------------=

#include "AccessFormatPlugin.h"
#include "SerializeEx.h"
#include "Date.h"
#include "Table.h"
#include "Options.h"
#include <string.h>

// Maximum number of fields each record can have...
#define MAX_FIELDS 30

enum IISFieldID {
  IIS_FIELD_TERMINATOR = -1,  // Terminates Fields array...

  IIS_DATE, IIS_TIME, IIS_CLIENT_IP, IIS_USERNAME, IIS_SITENAME, 
  IIS_COMPUTERNAME, IIS_SERVER_IP, IIS_SERVER_PORT, IIS_REQ_METHOD, 
  IIS_URI_STEM, IIS_URI_QUERY, IIS_STATUS, IIS_WIN32_STATUS, IIS_BYTES_SENT, 
  IIS_BYTES_READ, IIS_TIME_TAKEN, IIS_PROTO_VERSION, IIS_SERVER_HOST, 
  IIS_USER_AGENT, IIS_COOKIE, IIS_REFERER
};

class AfIISAccessFormat : public AccessFormatPluginImpl<AfIISAccessFormat> {
public :
  inline static unsigned int GetCurrentVersion() {
    return 100;                       // Version 1.00
  }

  inline static const char *GetPluginName() {
    return "MicrosoftIISLogFormat";
  }

  inline static int GetPriority() { return 100; }

  AfIISAccessFormat(int &CreateError) {
    CreateError = 0;
    ResetState();
  }

  AfIISAccessFormat(const String &ExLine, int &CreateError) {
    // The prefix that Microsoft IIS logs start with...
    const String Prefix("#Software: Microsoft Internet Information Services");
    CreateError = ExLine.nCompare(Prefix, Prefix.Length());

    if (!CreateError) ResetState();
  }

  virtual ~AfIISAccessFormat() {};

  virtual void operator=(const AccessFormatPlugin &F);
  virtual int ParseAccess(String &Line);

  //virtual const String &GetField(int FieldNo) const;
  //virtual void SetField(int FieldNo, const String &FieldVal);


  virtual const String &GetHost()       const { return Host; }
  virtual       String &GetHost()             { return Host; }
  virtual const String &GetAuth()       const { return Auth; }
  virtual       String &GetAuth()             { return Auth;  }
  virtual const Date   &GetDate()       const { return RDate; }
  virtual       Date   &GetDate()             { return RDate; }

  virtual const String &GetRetType()    const { return RetrievalType; }
  virtual       String &GetRetType()          { return RetrievalType; }
  virtual const String &GetURL()        const { return URL; }
  virtual       String &GetURL()              { return URL; }
  virtual const String &GetProtocol()   const { return Protocol; }
  virtual       String &GetProtocol()         { return Protocol; }
  virtual const String &GetDomain()     const { return Domain; }
  virtual       String &GetDomain()           { return Domain; }
  virtual const String &GetReferrer()   const { return Referrer; }
  virtual       String &GetReferrer()         { return Referrer; }
  virtual const String &GetBrowser()    const { return Browser; }
  virtual       String &GetBrowser()          { return Browser; }

  virtual       int GetStatusCode()     const { return StatusCode; }
  virtual       int GetLength()         const { return FileLength; }

  virtual void SetStatusCode(int S)         { StatusCode = S; }
  virtual void SetLength(int L)             { FileLength = L; }
  virtual void SetDomain(const String &d)   { DefaultDomain = d; }

  // We want to save and load the field configuration of the access log...
  virtual void ResetState();
  virtual void LoadState(Serialize &I);
  virtual void SaveState(Serialize &O);

private :
  void LookForFields(String &Line);
  static void DoURL(String &U);

  IISFieldID Fields[MAX_FIELDS];

private :
  String DefaultDomain;


  // Data //
  String Host;            // Host that downloaded the page
  String Auth;            // Username/Password if authenticated
  Date   RDate;           // Retrieval Date/Time
  String RetrievalType;   // Method used to retrieve
  String URL;             // URL Retrieved
  String Protocol;        // Protocol used to retrieve
  String Domain;          // Domain, common across the whole log file...
  
  String Browser;         // Browser info string...
  String Referrer;        // URL of referring page...

  int StatusCode;         // Status code of operation
  int FileLength;         // Length of file downloaded
};

// INIT_PLUGINS includes the AccessLog plugin in the internal list of 
// available plugins
//
INIT_PLUGIN(AfIISAccessFormat);


void AfIISAccessFormat::LookForFields(String &Line) {
  if (Line.nCompare("#Fields: ", 9)) 
    return;  // Must be the version/software line or some other header info

  Line.Right(Line.Length()-9);  // Strip off prefix...
  Table<String, IISFieldID> QuickLookup;

  static const struct {
    const char *FieldName;
    IISFieldID FieldID;
  } FieldNames[] = {
    { "date",            IIS_DATE }, 
    { "time",            IIS_TIME }, 
    { "c-ip",            IIS_CLIENT_IP }, 
    { "cs-username",     IIS_USERNAME },
    { "s-sitename",      IIS_SITENAME }, 
    { "s-computername",  IIS_COMPUTERNAME }, 
    { "s-ip",            IIS_SERVER_IP }, 
    { "s-port",          IIS_SERVER_PORT },
    { "cs-method",       IIS_REQ_METHOD }, 
    { "cs-uri-stem",     IIS_URI_STEM }, 
    { "cs-uri-query",    IIS_URI_QUERY }, 
    { "sc-status",       IIS_STATUS }, 
    { "sc-win32-status", IIS_WIN32_STATUS }, 
    { "sc-bytes",        IIS_BYTES_SENT }, 
    { "cs-bytes",        IIS_BYTES_READ }, 
    { "time-taken",      IIS_TIME_TAKEN }, 
    { "cs-version",      IIS_PROTO_VERSION }, 
    { "cs-host",         IIS_SERVER_HOST }, 
    { "cs(User-Agent)",  IIS_USER_AGENT }, 
    { "cs(Cookie)",      IIS_COOKIE }, 
    { "cs(Referer)",     IIS_REFERER }
  };


  // Make our array into a hash table so that we have decent lookup time...
  for (unsigned i = 0; i < sizeof(FieldNames)/sizeof(FieldNames[0]); i++) 
    QuickLookup.SetElementTo(FieldNames[i].FieldName, FieldNames[i].FieldID);

  QuickLookup.SetDefault(IIS_FIELD_TERMINATOR);

  int FieldNum = 0;
  // Wow, it does start with fields.  Parse the field description list now...
  while (Line.Length()) {
    String Field;
    Line.Tokenize(" ", Field);

    Fields[FieldNum++] = QuickLookup[Field];
    if (FieldNum >= MAX_FIELDS-1) {  // -1 because we want to store terminator
      //if (MSOptions::Verbose)
      cout << "Increase MAX_FIELDS constant in " __FILE__ " because I can't "
           << "process all the fields in your IIS log file!!!\n";
      break;
    }
  }

  Fields[FieldNum] = IIS_FIELD_TERMINATOR;  // Terminator
}


/* NEED TO IGNORE THESE RECORDS!!!!:
#Software: Microsoft Internet Information Services 5.0
#Version: 1.0
#Date: 2000-10-15 00:18:20
#SubComponent: Process Accounting
#Fields: date time s-event s-process-type s-user-time s-kernel-time s-page-faults s-total-procs s-active-procs s-stopped-procs
2000-10-15 00:18:20 Periodic-Log All 00.075% 00.028% 430437 671 1 0
*/


// Decode improperly URLized fields like the Browser type...
void AfIISAccessFormat::DoURL(String &U) {
  int Length = U.Length();
  for (int i = 0; i < Length; i++)
    if (U[i] == '+') U[i] = ' ';
}


int AfIISAccessFormat::ParseAccess(String &Line) {
  if (Line.Length() == 0) return -1;
  if (Line[0] == '#') {
    LookForFields(Line);
    return -1;
  }

  Domain = Host = Auth = RetrievalType = URL = Protocol = Browser = 
    Referrer = "";

  StatusCode = FileLength = 0;

  String Field;
  for (int i = 0; Fields[i] != IIS_FIELD_TERMINATOR; i++) {
    Line.Tokenize(" ", Field);

    if (Field.Length() == 1 && 
        Field[0] == '-') continue; // Try again... empty field.

    switch (Fields[i]) {
    case IIS_DATE:   // Field is of the nice format YYYY:MM:DD
      { unsigned Year  = (unsigned)Field.atoi(); Field.Right(5);
        unsigned Month = (unsigned)Field.atoi(); Field.Right(2);
        unsigned Day   = (unsigned)Field.atoi();
        if (Year < 1970 || Year > 2500 || 
            Month == 0 || Month > 12 || 
            Day == 0 || Day > 31) return -1;  // Invalid access
        RDate = Date(Day, Month, Year);
      }
      break;

    case IIS_TIME:   // Field is of the nice format HH:MM:SS
      RDate.SetHour  (Field.atoi()); Field.Right(5);
      RDate.SetMinute(Field.atoi()); Field.Right(2);
      RDate.SetSecond(Field.atoi());
      break;

    case IIS_CLIENT_IP:     Host = Field; break;
    case IIS_USERNAME:      Auth = Field; break;
    case IIS_URI_STEM:      URL  = Field + URL; break;
    case IIS_URI_QUERY:     URL += "?"+Field; break;
    case IIS_SERVER_HOST:   Domain = "http://"+Field+Domain; break;
    case IIS_SERVER_PORT:   if (Field.atoi() != 80) Domain += ":"+Field; break;
    case IIS_REQ_METHOD:    RetrievalType = Field; break;
    case IIS_STATUS:        StatusCode = Field.atoi(); break;
    case IIS_BYTES_SENT:    FileLength = Field.atoi(); break;
    case IIS_USER_AGENT:    DoURL(Field); Browser = Field; break;
    case IIS_REFERER:       Referrer = Field; break;
    case IIS_PROTO_VERSION: Protocol = Field; break;

    default: break; // Ignore these types...
      /*
    IIS_DATE, IIS_TIME, IIS_SITENAME, IIS_COMPUTERNAME, IIS_WIN32_STATUS, IIS_BYTES_READ, IIS_TIME_TAKEN, IIS_COOKIE, 
      */
    }
  }

  if (Domain.Length() == 0) Domain = DefaultDomain;

  if (URL.Length() == 0) {
    return -1;
  }

  //  cout << "Read access for IIS Log: "; print(); cout << endl;
  return 0;
}

void AfIISAccessFormat::operator=(const AccessFormatPlugin &f) {
  const AfIISAccessFormat &F = (const AfIISAccessFormat &)f;

  Host          = F.Host;
  Auth          = F.Auth;
  RDate         = F.RDate;
  RetrievalType = F.RetrievalType;
  URL           = F.URL;
  Protocol      = F.Protocol;
  StatusCode    = F.StatusCode;
  FileLength    = F.FileLength;  
  Browser       = F.Browser;
  Referrer      = F.Referrer;
}

//=----------------------------------------------------------------------------=
//  State Management Routines...
//=----------------------------------------------------------------------------=
// 
// We want to be able to save and load the field configuration of an access log
// so that we don't have to reparse the first few lines when we continue reading
// from a log file...
//
void AfIISAccessFormat::ResetState() {   // Default state...
  StatusCode = 0;
  FileLength = 0;
  
  // Default field configuration!
  Fields[0] = IIS_DATE; Fields[1] = IIS_TIME; Fields[2] = IIS_CLIENT_IP;
  Fields[3] = IIS_USERNAME; Fields[4] = IIS_SITENAME; 
  Fields[5] = IIS_COMPUTERNAME; Fields[6] = IIS_SERVER_IP; 
  Fields[7] = IIS_SERVER_PORT; Fields[8] = IIS_REQ_METHOD;
  Fields[9] = IIS_URI_STEM; Fields[10] = IIS_URI_QUERY; 
  Fields[11] = IIS_STATUS; Fields[12] = IIS_WIN32_STATUS;
  Fields[13] = IIS_BYTES_SENT; Fields[14] = IIS_BYTES_READ;
  Fields[15] = IIS_TIME_TAKEN; Fields[16] = IIS_PROTO_VERSION;
  Fields[17] = IIS_SERVER_HOST; Fields[18] = IIS_USER_AGENT;
  Fields[19] = IIS_COOKIE; Fields[20] = IIS_REFERER;
  Fields[21] = IIS_FIELD_TERMINATOR;
}


// Need to be able to serialize and deserialize individual elements to be able
// to save and restore a whole array...
//
Serialize &operator<<(Serialize &I, const IISFieldID &ID) {
  return I << (int)ID;
}

Serialize &operator>>(Serialize &O, IISFieldID &ID) {
  int id; O >> id;  ID = (IISFieldID)id;
  return O;
}


void AfIISAccessFormat::SaveState(Serialize &O) {
  SerializeArray(O, Fields, MAX_FIELDS);
}

void AfIISAccessFormat::LoadState(Serialize &I) {
  DeSerializeArray(I, Fields, MAX_FIELDS);
}
