"""Text parser plugin for Microsoft IIS log files."""

import pyparsing

from dfdatetime import time_elements as dfdatetime_time_elements

from plaso.containers import events
from plaso.lib import errors
from plaso.parsers import text_parser
from plaso.parsers.text_plugins import interface


class IISEventData(events.EventData):
  """IIS log event data.

  Attributes:
    cs_cookie (str): Content of a sent or received cookie.
    cs_host (str): HTTP host header name.
    cs_referrer (str): Site that referred to the requested site.
    cs_uri_query (str): URI query that was requested.
    cs_username (str): Username of the authenticated user that accessed
        the server, where anonymous users are indicated by a hyphen.
    dest_ip (str): IP address of the server that generated the logged activity.
    dest_port (str): Server port number.
    http_method (str): HTTP request method, such as GET or POST.
    http_status (str): HTTP status code that was returned by the server.
    last_written_time (dfdatetime.DateTimeValues): entry last written date and
        time.
    protocol_version (str): HTTP protocol version that was used.
    received_bytes (str): Number of bytes received and processed by the server.
    requested_uri_stem (str): File requested, such as index.php or Default.htm
    s_computername (str): Name of the server that generated the logged activity.
    sc_substatus (str):  HTTP substatus error code that was returned by the
        server.
    sc_win32_status (str): Windows status code of the server.
    sent_bytes (str): Number of bytes sent by the server.
    source_ip (str): IP address of the client that made the request.
    s_sitename (str): Service name and instance number that was running on the
        client.
    time_taken (str): Time taken, in milliseconds, to process the request.
    user_agent (str): User agent that was used.
  """

  DATA_TYPE = 'iis:log:line'

  def __init__(self):
    """Initializes event data."""
    super().__init__(data_type=self.DATA_TYPE)
    self.cs_cookie = None
    self.cs_host = None
    self.cs_referrer = None
    self.cs_uri_query = None
    self.cs_username = None
    self.dest_ip = None
    self.dest_port = None
    self.http_method = None
    self.http_status = None
    self.last_written_time = None
    self.protocol_version = None
    self.received_bytes = None
    self.requested_uri_stem = None
    self.s_computername = None
    self.sc_substatus = None
    self.sc_win32_status = None
    self.sent_bytes = None
    self.source_ip = None
    self.s_sitename = None
    self.time_taken = None
    self.user_agent = None


class WinIISTextPlugin(interface.TextPlugin):
  """Text parser plugin for Microsoft IIS log files."""

  NAME = 'winiis'
  DATA_FORMAT = 'Microsoft IIS log file'

  # Log file are all extended ASCII encoded unless UTF-8 is explicitly enabled.
  # TODO: fix
  ENCODING = 'utf-8'

  _BLANK = pyparsing.Literal('-')

  _HTTP_METHOD = pyparsing.Word(pyparsing.alphanums + '-_') | _BLANK

  _INTEGER = pyparsing.Word(pyparsing.nums).set_parse_action(
      lambda tokens: int(tokens[0], 10)) | _BLANK

  _TWO_DIGITS = pyparsing.Word(pyparsing.nums, exact=2).set_parse_action(
      lambda tokens: int(tokens[0], 10))

  _FOUR_DIGITS = pyparsing.Word(pyparsing.nums, exact=4).set_parse_action(
      lambda tokens: int(tokens[0], 10))

  _IP_ADDRESS = (
      pyparsing.pyparsing_common.ipv4_address |
      pyparsing.pyparsing_common.ipv6_address | _BLANK)

  PORT = pyparsing.Word(pyparsing.nums, max=6).set_parse_action(
      lambda tokens: int(tokens[0], 10)) | _BLANK

  # Username can consist of: "domain.username", "domain\username",
  # "domain\user$", "domain/user", "user@domain" or "-" for an anonymous user.
  _USERNAME = pyparsing.Word(pyparsing.alphanums + '-.\\$@/') | _BLANK

  _URI_SAFE_CHARACTERS = '/.?&+;_=()-:,%'

  _URI = pyparsing.Word(pyparsing.alphanums + _URI_SAFE_CHARACTERS) | _BLANK

  _URI_STEM = (pyparsing.Word(
      pyparsing.alphanums + _URI_SAFE_CHARACTERS + '$') | _BLANK)

  _UA = pyparsing.Word(
      pyparsing.alphanums + _URI_SAFE_CHARACTERS + '[]') | _BLANK

  _COOKIE = pyparsing.Word(
      pyparsing.alphanums + _URI_SAFE_CHARACTERS + '@{}"\\') | _BLANK

  # Per https://blogs.iis.net/nazim/use-of-special-characters-like-in-an-iis-url
  # IIS does not require that a query comply with RFC1738 restrictions on valid
  # URI characters
  _QUERY = (pyparsing.Word(
      pyparsing.alphanums + _URI_SAFE_CHARACTERS + '{}|\\^~[]`\'"<>@$') |
           _BLANK)

  _DATE = (
      _FOUR_DIGITS + pyparsing.Suppress('-') +
      _TWO_DIGITS + pyparsing.Suppress('-') + _TWO_DIGITS)

  _TIME = (
      _TWO_DIGITS + pyparsing.Suppress(':') +
      _TWO_DIGITS + pyparsing.Suppress(':') + _TWO_DIGITS)

  _DATE_TIME_METADATA = (
      pyparsing.Suppress('Date: ') +
      _DATE.set_results_name('date') +
      _TIME.set_results_name('time'))

  _FIELDS_METADATA = (
      pyparsing.Suppress('Fields: ') +
      pyparsing.restOfLine().set_results_name('fields'))

  _METADATA = (
      _DATE_TIME_METADATA | _FIELDS_METADATA | pyparsing.restOfLine())

  _END_OF_LINE = pyparsing.Suppress(pyparsing.LineEnd())

  _COMMENT_LOG_LINE = pyparsing.Suppress('#') + _METADATA + _END_OF_LINE

  # IIS 6.x fields: date time s-sitename s-ip cs-method cs-uri-stem
  # cs-uri-query s-port cs-username c-ip cs(User-Agent) sc-status
  # sc-substatus sc-win32-status

  _IIS_6_0_LOG_LINE = (
      _DATE.set_results_name('date') +
      _TIME.set_results_name('time') +
      _URI.set_results_name('s_sitename') +
      _IP_ADDRESS.set_results_name('dest_ip') +
      _HTTP_METHOD.set_results_name('http_method') +
      _URI.set_results_name('cs_uri_stem') +
      _URI.set_results_name('cs_uri_query') +
      PORT.set_results_name('dest_port') +
      _USERNAME.set_results_name('cs_username') +
      _IP_ADDRESS.set_results_name('source_ip') +
      _UA.set_results_name('user_agent') +
      _INTEGER.set_results_name('sc_status') +
      _INTEGER.set_results_name('sc_substatus') +
      _INTEGER.set_results_name('sc_win32_status') +
      _END_OF_LINE)

  # IIS 7.x fields: date time s-ip cs-method cs-uri-stem cs-uri-query
  # s-port cs-username c-ip cs(User-Agent) sc-status sc-substatus
  # sc-win32-status time-taken

  _LOG_LINE_STRUCTURES = {}

  # Common fields. Set results name with underscores, not hyphens because regex
  # will not pick them up.

  _LOG_LINE_STRUCTURES['date'] = _DATE.set_results_name('date')
  _LOG_LINE_STRUCTURES['time'] = _TIME.set_results_name('time')
  _LOG_LINE_STRUCTURES['s-sitename'] = _URI.set_results_name('s_sitename')
  _LOG_LINE_STRUCTURES['s-ip'] = _IP_ADDRESS.set_results_name('dest_ip')
  _LOG_LINE_STRUCTURES['cs-method'] = _HTTP_METHOD.set_results_name(
      'http_method')
  _LOG_LINE_STRUCTURES['cs-uri-stem'] = _URI_STEM.set_results_name(
      'requested_uri_stem')
  _LOG_LINE_STRUCTURES['cs-uri-query'] = _QUERY.set_results_name('cs_uri_query')
  _LOG_LINE_STRUCTURES['s-port'] = PORT.set_results_name('dest_port')
  _LOG_LINE_STRUCTURES['cs-username'] = _USERNAME.set_results_name(
      'cs_username')
  _LOG_LINE_STRUCTURES['c-ip'] = _IP_ADDRESS.set_results_name('source_ip')
  _LOG_LINE_STRUCTURES['cs(User-Agent)'] = _UA.set_results_name('user_agent')
  _LOG_LINE_STRUCTURES['sc-status'] = _INTEGER.set_results_name('http_status')
  _LOG_LINE_STRUCTURES['sc-substatus'] = _INTEGER.set_results_name(
      'sc_substatus')
  _LOG_LINE_STRUCTURES['sc-win32-status'] = _INTEGER.set_results_name(
      'sc_win32_status')

  # Less common fields.

  _LOG_LINE_STRUCTURES['s-computername'] = _URI.set_results_name(
      's_computername')
  _LOG_LINE_STRUCTURES['sc-bytes'] = _INTEGER.set_results_name('sent_bytes')
  _LOG_LINE_STRUCTURES['cs-bytes'] = _INTEGER.set_results_name('received_bytes')
  _LOG_LINE_STRUCTURES['time-taken'] = _INTEGER.set_results_name('time_taken')
  _LOG_LINE_STRUCTURES['cs-version'] = _URI.set_results_name('protocol_version')
  _LOG_LINE_STRUCTURES['cs-host'] = _URI.set_results_name('cs_host')
  _LOG_LINE_STRUCTURES['cs(Cookie)'] = _COOKIE.set_results_name('cs_cookie')
  _LOG_LINE_STRUCTURES['cs(Referrer)'] = _URI.set_results_name('cs_referrer')
  _LOG_LINE_STRUCTURES['cs(Referer)'] = _URI.set_results_name('cs_referrer')

  # Define the available log line structures. Default to the IIS v. 6.0
  # common format.

  _HEADER_GRAMMAR = pyparsing.OneOrMore(_COMMENT_LOG_LINE)

  _LINE_STRUCTURES = [('log_line', _IIS_6_0_LOG_LINE)]

  _COMMENT_SOFTWARE_LINE = (
      pyparsing.Regex(
          '#Software: Microsoft Internet Information Services [0-9]+.[0-9]+') +
      _END_OF_LINE)

  VERIFICATION_GRAMMAR = (
      pyparsing.ZeroOrMore(
          pyparsing.Regex('#(Date|Fields|Version): .*') + _END_OF_LINE) +
      _COMMENT_SOFTWARE_LINE)

  VERIFICATION_LITERALS = [
      '#Software: Microsoft Internet Information Services ']

  def __init__(self):
    """Initializes a parser."""
    super().__init__()
    self._day_of_month = None
    self._month = None
    self._year = None

  def _ParseFieldsMetadata(self, parser_mediator, fields):
    """Parses the fields metadata and updates the log line definition to match.

    Args:
      parser_mediator (ParserMediator): mediates interactions between parsers
          and other components, such as storage and dfVFS.
      fields (str): field definitions.
    """
    log_line_structure = pyparsing.Empty()
    for member in fields.split(' '):
      if not member:
        continue

      field_structure = self._LOG_LINE_STRUCTURES.get(member)
      if not field_structure:
        field_structure = self._URI
        parser_mediator.ProduceExtractionWarning(
            f'missing definition for field: {member:s} defaulting to URI')

      log_line_structure += field_structure

    log_line_structure += self._END_OF_LINE

    self._SetLineStructures([('log_line', log_line_structure)])

  def _ParseHeader(self, parser_mediator, text_reader):
    """Parses a text-log file header.

    Args:
      parser_mediator (ParserMediator): mediates interactions between parsers
          and other components, such as storage and dfVFS.
      text_reader (EncodedTextReader): text reader.

    Raises:
      ParseError: when the header cannot be parsed.
    """
    try:
      structure_generator = self._HEADER_GRAMMAR.scan_string(
          text_reader.lines, max_matches=1)
      structure, start, end = next(structure_generator)

    except StopIteration:
      structure = None

    except pyparsing.ParseException as exception:
      raise errors.ParseError(exception)

    if not structure or start != 0:
      raise errors.ParseError('No match found.')

    date_elements_tuple = self._GetValueFromStructure(structure, 'date')
    if date_elements_tuple:
      self._year, self._month, self._day_of_month = date_elements_tuple

    fields = self._GetValueFromStructure(structure, 'fields', default_value='')
    fields = fields.strip()
    if fields:
      self._ParseFieldsMetadata(parser_mediator, fields)

    text_reader.SkipAhead(end)

  def _ParseLogLine(self, parser_mediator, structure):
    """Parse a single log line.

    Args:
      parser_mediator (ParserMediator): mediates interactions between parsers
          and other components, such as storage and dfVFS.
      structure (pyparsing.ParseResults): tokens from a parsed log line.
    """
    event_data = IISEventData()
    event_data.cs_cookie = self._GetValueFromStructure(structure, 'cs_cookie')
    event_data.cs_host = self._GetValueFromStructure(structure, 'cs_host')
    event_data.cs_referrer = self._GetValueFromStructure(
        structure, 'cs_referrer')
    event_data.cs_uri_query = self._GetValueFromStructure(
        structure, 'cs_uri_query')
    event_data.cs_username = self._GetValueFromStructure(
        structure, 'cs_username')
    event_data.dest_ip = self._GetValueFromStructure(structure, 'dest_ip')
    event_data.dest_port = self._GetValueFromStructure(structure, 'dest_port')
    event_data.http_method = self._GetValueFromStructure(
        structure, 'http_method')
    event_data.http_status = self._GetValueFromStructure(
        structure, 'http_status')
    event_data.protocol_version = self._GetValueFromStructure(
        structure, 'protocol_version')
    event_data.last_written_time = self._ParseTimeElements(structure)
    event_data.received_bytes = self._GetValueFromStructure(
        structure, 'received_bytes')
    event_data.requested_uri_stem = self._GetValueFromStructure(
        structure, 'requested_uri_stem')
    event_data.s_computername = self._GetValueFromStructure(
        structure, 's_computername')
    event_data.sc_substatus = self._GetValueFromStructure(
        structure, 'sc_substatus')
    event_data.sc_win32_status = self._GetValueFromStructure(
        structure, 'sc_win32_status')
    event_data.sent_bytes = self._GetValueFromStructure(structure, 'sent_bytes')
    event_data.source_ip = self._GetValueFromStructure(structure, 'source_ip')
    event_data.s_sitename = self._GetValueFromStructure(structure, 's_sitename')
    event_data.time_taken = self._GetValueFromStructure(structure, 'time_taken')
    event_data.user_agent = self._GetValueFromStructure(structure, 'user_agent')

    parser_mediator.ProduceEventData(event_data)

  def _ParseRecord(self, parser_mediator, key, structure):
    """Parses a pyparsing structure.

    Args:
      parser_mediator (ParserMediator): mediates interactions between parsers
          and other components, such as storage and dfVFS.
      key (str): name of the parsed structure.
      structure (pyparsing.ParseResults): tokens from a parsed log line.

    Raises:
      ParseError: if the structure cannot be parsed.
    """
    self._ParseLogLine(parser_mediator, structure)

  def _ParseTimeElements(self, structure):
    """Parses date and time elements of a log line.

    Args:
      structure (pyparsing.ParseResults): tokens from a parsed log line.

    Returns:
      dfdatetime.TimeElements: date and time value.

    Raises:
      ParseError: if a valid date and time value cannot be derived from
          the time elements.
    """
    try:
      time_elements_structure = self._GetValueFromStructure(structure, 'time')

      hours, minutes, seconds = time_elements_structure

      date_elements_structure = self._GetValueFromStructure(structure, 'date')
      if date_elements_structure:
        year, month, day_of_month = date_elements_structure

        time_elements_tuple = (
            year, month, day_of_month, hours, minutes, seconds)
      else:
        time_elements_tuple = (
             self._year, self._month, self._day_of_month, hours, minutes,
             seconds)

      return dfdatetime_time_elements.TimeElements(
          time_elements_tuple=time_elements_tuple)

    except (IndexError, TypeError, ValueError) as exception:
      raise errors.ParseError(
          f'Unable to parse time elements with error: {exception!s}')

  def _ResetState(self):
    """Resets stored values."""
    self._day_of_month = None
    self._month = None
    self._year = None

    self._SetLineStructures(self._LINE_STRUCTURES)

  def CheckRequiredFormat(self, parser_mediator, text_reader):
    """Check if the log record has the minimal structure required by the plugin.

    Args:
      parser_mediator (ParserMediator): mediates interactions between parsers
          and other components, such as storage and dfVFS.
      text_reader (EncodedTextReader): text reader.

    Returns:
      bool: True if this is the correct plugin, False otherwise.
    """
    try:
      self._VerifyString(text_reader.lines)
    except errors.ParseError:
      return False

    self._ResetState()

    return True


text_parser.TextLogParser.RegisterPlugin(WinIISTextPlugin)
