"""Text parser plugin for AWS ELB access logs.

The AWS documentation is not clear about the meaning of the "target_port_list"
field. The assumption is that it refers to a list of possible backend instances'
IP addresses that could receive the client's request. This parser stores the
"target_port_list" data in the "destination_list" attribute of an EventData
object.

Also see:
  https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-access-logs.html
  https://docs.aws.amazon.com/elasticloadbalancing/latest/classic/access-log-collection.html
  https://docs.aws.amazon.com/elasticloadbalancing/latest/network/load-balancer-access-logs.html
"""

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 AWSELBEventData(events.EventData):
  """AWS Elastic Load Balancer access log event data.

  Attributes:
    actions_executed (str): The actions taken when processing the request.
    alpn_back_end_protocol (str): The application protocol negotiated with
        the target, in string format. If no ALPN policy is configured in
        the TLS listener, no matching protocol is found, or no valid protocol
        list is sent, this value is set to -.
        (only for network load balancer logs)
    alpn_client_preference_list (str): The value of the
        application_layer_protocol_negotiation extension in the client hello
        message. This value is URL-encoded. Each protocol is enclosed in double
        quotes and protocols are separated by a comma. If no ALPN policy is
        configured in the TLS listener, no valid client hello message is sent,
        or the extension is not present, this value is set to -. The string is
        truncated if it is longer than 256 bytes.
        (only for network load balancer logs)
    alpn_front_end_protocol (str): The application protocol negotiated with
        the client, in string format. If no ALPN policy is configured in
        the TLS listener, no matching protocol is found, or no valid protocol
        list is sent, this value is set to -.
        (only for network load balancer logs)
    chosen_cert_arn (str): The ARN of the certificate
        presented to the source.
    chosen_cert_serial (str): Reserved for future use.
        This value is always set to -.
        (only for network load balancer logs)
    classification (str): The classification for desync mitigation.
    classification_reason (str): The classification reason code.
    connection_duration (str): duration of the connection to complete, from
        start to closure, in milliseconds.
        (only for network load balancer logs)
    destination_group_arn (str): The Amazon Resource Name (ARN) of the
        destination group.
    destination_ip_address (str): The IP address of the destination
        that processed this request.
    destination_list (str): A space-delimited list of IP addresses
        and ports for the destinations that processed this request.
    destination_port (int): The port of the destination that processed
        this request.
    destination_processing_duration (str): duration from the time the load
        balancer sent the request to a destination until the destination
        started to send the response headers.
    destination_status_code (int): The status code of the response
        from the destination.
    destination_status_code_list (str): A space-delimited list of status codes.
    domain_name (str): The SNI domain provided by the
        source during the TLS handshake.
    error_reason (str): The error reason code, enclosed in
        double quotes.
    handshake_duration (str): duration of the handshake to complete after
        the TCP connection is established, including client-side delays,
        in milliseconds. This time is included in the connection_duration field.
        (only for network load balancer logs)
    incoming_tls_alert (str): The integer value of TLS alerts received by the
        load balancer from the client, if present.
        (only for network load balancer logs)
    listener (str): The resource ID of the TLS listener for the connection.
        (only for network load balancer logs)
    matched_rule_priority (int): The priority value of the rule that
        matched the request.
    received_bytes (int): The size of the request, in bytes, received from
        the source.
    redirect_url (str): The URL of the redirect destination.
    request_processing_duration (str): total duration from the time the load
        balancer received the request until the time it sent the request to
        a destination.
    request_time (dfdatetime.DateTimeValues): date and time a request
        was sent.
    request_type (str): The type of request or connection.
    resource_identifier (str): The resource ID of the load balancer.
    response_processing_duration (str): duration of processing a response.
    response_time (dfdatetime.DateTimeValues): date and time a response
        was sent.
    sent_bytes (int): The size of the response, in bytes, sent to the source.
    ssl_cipher (str): The SSL cipher of the HTTPS listener.
    ssl_protocol (str): The SSL protocol of the HTTPS listener.
    source_ip_address (str): The IP address of the requesting source.
    source_port (int): The port of the requesting source.
    tls_cipher (str): The cipher suite negotiated with the client, in OpenSSL
        format. If TLS negotiation does not complete, this value is set to -.
        (only for network load balancer logs)
    tls_named_group (str): Reserved for future use.
        This value is always set to -.
        (only for network load balancer logs)
    tls_protocol_version (str): The TLS protocol negotiated with the client,
        in string format. If TLS negotiation does not complete,
        this value is set to -.
        (only for network load balancer logs)
    trace_identifier (str): The contents of the X-Amzn-Trace-Id header.
    user_agent (str): A User-Agent string.
    version (str): The version of the log entry.
        (only for network load balancer logs)
  """

  DATA_TYPE = 'aws:elb:access'

  def __init__(self):
    """Initializes event data."""
    super().__init__(data_type=self.DATA_TYPE)
    self.actions_executed = None
    self.alpn_back_end_protocol = None
    self.alpn_client_preference_list = None
    self.alpn_front_end_protocol = None
    self.chosen_cert_arn = None
    self.chosen_cert_serial = None
    self.classification = None
    self.classification_reason = None
    self.connection_duration = None
    self.destination_group_arn = None
    self.destination_ip_address = None
    self.destination_list = None
    self.destination_port = None
    self.destination_processing_duration = None
    self.destination_status_code = None
    self.destination_status_code_list = None
    self.domain_name = None
    self.elb_status_code = None
    self.error_reason = None
    self.handshake_duration = None
    self.incoming_tls_alert = None
    self.listener = None
    self.matched_rule_priority = None
    self.received_bytes = None
    self.redirect_url = None
    self.request = None
    self.request_processing_duration = None
    self.request_time = None
    self.request_type = None
    self.resource_identifier = None
    self.response_processing_duration = None
    self.response_time = None
    self.sent_bytes = None
    self.source_ip_address = None
    self.source_port = None
    self.ssl_cipher = None
    self.ssl_protocol = None
    self.tls_cipher = None
    self.tls_named_group = None
    self.tls_protocol_version = None
    self.trace_identifier = None
    self.user_agent = None
    self.version = None


class AWSELBTextPlugin(interface.TextPlugin):
  """Text parser plugin for AWS ELB access log files."""

  NAME = 'aws_elb_access'
  DATA_FORMAT = 'AWS ELB Access log file'

  ENCODING = 'utf-8'

  _BLANK = pyparsing.Literal('"-"') | pyparsing.Literal('-')

  _WORD = pyparsing.Word(pyparsing.printables) | _BLANK

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

  _UNSIGNED_INTEGER = _INTEGER | _BLANK

  _SIGNED_INTEGER = pyparsing.Word('-', pyparsing.nums) | _UNSIGNED_INTEGER

  _FLOATING_POINT = (
      pyparsing.Word(pyparsing.nums + '.') | pyparsing.Literal('-1'))

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

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

  _SOURCE_IP_ADDRESS_AND_PORT = pyparsing.Group(
      _IP_ADDRESS.set_results_name('source_ip_address') +
      pyparsing.Suppress(':') + _PORT.set_results_name('source_port') | _BLANK)

  _DESTINATION_IP_ADDRESS_AND_PORT = pyparsing.Group(
      _IP_ADDRESS.set_results_name('destination_ip_address') +
      pyparsing.Suppress(':') + _PORT.set_results_name('destination_port') |
      _BLANK)

  # Date and time values are formatted as:
  # 2020-01-11T16:56:05.917294Z
  _DATE_TIME_ISOFORMAT_STRING = pyparsing.Regex(
      r'([0-9]{4}-[0-9]{2}-[0-9]{2}T'
      r'[0-9]{2}:[0-9]{2}:[0-9]{2}.[0-9]{6})Z')

  # Date and time values are formatted as:
  # 2022-12-20T02:59:40
  _DATE_TIME_ISOFORMAT_STRING_WITHOUT_TIMEZONE = pyparsing.Regex(
      r'([0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2})')

  _REQUEST = pyparsing.quotedString.set_results_name(
      'request').set_parse_action(pyparsing.remove_quotes)

  _USER_AGENT = pyparsing.quotedString.set_results_name(
      'user_agent').set_parse_action(pyparsing.remove_quotes)

  _ALPN_CLIENT_PREFERENCE_LIST = pyparsing.quotedString.set_results_name(
      'alpn_client_preference_list').set_parse_action(pyparsing.remove_quotes)

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

  # A log line is defined as in the AWS ELB documentation
  _APPLICATION_LOG_LINE = (
      _WORD.set_results_name('request_type') +
      _DATE_TIME_ISOFORMAT_STRING.set_results_name('response_time') +
      _WORD.set_results_name('resource_identifier') +
      _SOURCE_IP_ADDRESS_AND_PORT.set_results_name('source_ip_port') +
      _DESTINATION_IP_ADDRESS_AND_PORT.set_results_name('destination_ip_port') +
      _FLOATING_POINT.set_results_name('request_processing_duration') +
      _FLOATING_POINT.set_results_name('destination_processing_duration') +
      _FLOATING_POINT.set_results_name('response_processing_duration') +
      _UNSIGNED_INTEGER.set_results_name('elb_status_code') +
      _UNSIGNED_INTEGER.set_results_name('destination_status_code') +
      _UNSIGNED_INTEGER.set_results_name('received_bytes') +
      _UNSIGNED_INTEGER.set_results_name('sent_bytes') +
      _REQUEST +
      _USER_AGENT +
      _WORD.set_results_name('ssl_cipher') +
      _WORD.set_results_name('ssl_protocol') +
      _WORD.set_results_name('destination_group_arn') +
      pyparsing.quotedString.set_results_name(
          'trace_identifier').set_parse_action(pyparsing.remove_quotes) +
      pyparsing.quotedString.set_results_name(
          'domain_name').set_parse_action(pyparsing.remove_quotes) +
      pyparsing.quotedString.set_results_name(
          'chosen_cert_arn').set_parse_action(pyparsing.remove_quotes) +
      _SIGNED_INTEGER.set_results_name('matched_rule_priority') +
      _DATE_TIME_ISOFORMAT_STRING.set_results_name('request_time') +
      pyparsing.quotedString.set_results_name(
          'actions_executed').set_parse_action(pyparsing.remove_quotes) +
      pyparsing.quotedString.set_results_name(
          'redirect_url').set_parse_action(pyparsing.remove_quotes) +
      pyparsing.quotedString.set_results_name(
          'error_reason').set_parse_action(pyparsing.remove_quotes) +
      pyparsing.quotedString.set_results_name(
          'destination_list').set_parse_action(pyparsing.remove_quotes) +
      pyparsing.quotedString.set_results_name(
          'destination_status_code_list').set_parse_action(
              pyparsing.remove_quotes) +
      pyparsing.quotedString.set_results_name(
          'classification').set_parse_action(pyparsing.remove_quotes) +
      pyparsing.quotedString.set_results_name(
          'classification_reason').set_parse_action(pyparsing.remove_quotes) +
      _END_OF_LINE)

  _NETWORK_LOG_LINE = (
      _WORD.set_results_name('request_type') +
      _WORD.set_results_name('version') +
      _DATE_TIME_ISOFORMAT_STRING_WITHOUT_TIMEZONE.set_results_name(
          'response_time') +
      _WORD.set_results_name('resource_identifier') +
      _WORD.set_results_name('listener') +
      _SOURCE_IP_ADDRESS_AND_PORT.set_results_name('source_ip_port') +
      _DESTINATION_IP_ADDRESS_AND_PORT.set_results_name('destination_ip_port') +
      _UNSIGNED_INTEGER.set_results_name('connection_duration') +
      _UNSIGNED_INTEGER.set_results_name('handshake_duration') +
      _UNSIGNED_INTEGER.set_results_name('received_bytes') +
      _UNSIGNED_INTEGER.set_results_name('sent_bytes') +
      _WORD.set_results_name('incoming_tls_alert') +
      _WORD.set_results_name('chosen_cert_arn') +
      _WORD.set_results_name('chosen_cert_serial') +
      _WORD.set_results_name('tls_cipher') +
      _WORD.set_results_name('tls_protocol_version') +
      _WORD.set_results_name('tls_named_group') +
      _WORD.set_results_name('domain_name') +
      _WORD.set_results_name('alpn_front_end_protocol') +
      _WORD.set_results_name('alpn_back_end_protocol') +
      (_ALPN_CLIENT_PREFERENCE_LIST | pyparsing.Literal('-')) +
      _END_OF_LINE)

  _CLASSIC_LOG_LINE = (
      _DATE_TIME_ISOFORMAT_STRING.set_results_name('response_time') +
      _WORD.set_results_name('resource_identifier') +
      _SOURCE_IP_ADDRESS_AND_PORT.set_results_name('source_ip_port') +
      _DESTINATION_IP_ADDRESS_AND_PORT.set_results_name('destination_ip_port') +
      _FLOATING_POINT.set_results_name('request_processing_duration') +
      _FLOATING_POINT.set_results_name('destination_processing_duration') +
      _FLOATING_POINT.set_results_name('response_processing_duration') +
      _UNSIGNED_INTEGER.set_results_name('elb_status_code') +
      _UNSIGNED_INTEGER.set_results_name('destination_status_code') +
      _SIGNED_INTEGER.set_results_name('received_bytes') +
      _SIGNED_INTEGER.set_results_name('sent_bytes') +
      _REQUEST +
      _USER_AGENT +
      _WORD.set_results_name('ssl_cipher') +
      _WORD.set_results_name('ssl_protocol') +
      _END_OF_LINE)

  _LINE_STRUCTURES = [
      ('elb_application_accesslog', _APPLICATION_LOG_LINE),
      ('elb_classic_accesslog', _CLASSIC_LOG_LINE),
      ('elb_network_accesslog', _NETWORK_LOG_LINE)]

  VERIFICATION_GRAMMAR = (
      _APPLICATION_LOG_LINE ^ _CLASSIC_LOG_LINE ^ _NETWORK_LOG_LINE)

  def _GetValueFromGroup(self, structure, name, key_name):
    """Retrieves a value from a Pyparsing.Group structure.

    Args:
      structure (pyparsing.ParseResults): tokens from a parsed log line.
      name (str): name of the token.
      key_name (str): key name to retrieve the value of.

    Returns:
      object: value for the specified key or None if not available.
    """
    structure_value = self._GetValueFromStructure(structure, name)
    if structure_value is None:
      return None

    return structure_value.get(key_name)

  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.
    """
    destination_list = self._GetValueFromStructure(
        structure, 'destination_list')
    if destination_list:
      destination_list = destination_list.split()

    chosen_cert_serial = self._GetValueFromStructure(
        structure, 'chosen_cert_serial')
    if chosen_cert_serial == '-':
      chosen_cert_serial = None

    classification = self._GetValueFromStructure(structure, 'classification')
    if classification == '-':
      classification = None

    classification_reason = self._GetValueFromStructure(
        structure, 'classification_reason')
    if classification_reason == '-':
      classification_reason = None

    destination_status_code = self._GetValueFromStructure(
        structure, 'destination_status_code')
    if destination_status_code == '-':
      destination_status_code = None

    elb_status_code = self._GetValueFromStructure(structure, 'elb_status_code')
    if elb_status_code == '-':
      elb_status_code = None

    error_reason = self._GetValueFromStructure(structure, 'error_reason')
    if error_reason == '-':
      error_reason = None

    incoming_tls_alert = self._GetValueFromStructure(
        structure, 'incoming_tls_alert')
    if incoming_tls_alert == '-':
      incoming_tls_alert = None

    redirect_url = self._GetValueFromStructure(structure, 'redirect_url')
    if redirect_url == '-':
      redirect_url = None

    ssl_cipher = self._GetValueFromStructure(structure, 'ssl_cipher')
    if ssl_cipher == '-':
      ssl_cipher = None

    ssl_protocol = self._GetValueFromStructure(structure, 'ssl_protocol')
    if ssl_protocol == '-':
      ssl_protocol = None

    tls_named_group = self._GetValueFromStructure(structure, 'tls_named_group')
    if tls_named_group == '-':
      tls_named_group = None

    user_agent = self._GetValueFromStructure(structure, 'user_agent')
    if user_agent == '-':
      user_agent = None

    event_data = AWSELBEventData()
    event_data.request_type = self._GetValueFromStructure(
        structure, 'request_type')
    event_data.resource_identifier = self._GetValueFromStructure(
        structure, 'resource_identifier')
    event_data.source_ip_address = self._GetValueFromGroup(
        structure, 'source_ip_port', 'source_ip_address')
    event_data.source_port = self._GetValueFromGroup(
        structure, 'source_ip_port', 'source_port')
    event_data.destination_ip_address = self._GetValueFromGroup(
        structure, 'destination_ip_port', 'destination_ip_address')
    event_data.destination_port = self._GetValueFromGroup(
        structure, 'destination_ip_port', 'destination_port')
    event_data.request_processing_duration = self._GetValueFromStructure(
        structure, 'request_processing_duration')
    event_data.destination_processing_duration = self._GetValueFromStructure(
        structure, 'destination_processing_duration')
    event_data.response_processing_duration = self._GetValueFromStructure(
        structure, 'response_processing_duration')
    event_data.elb_status_code = elb_status_code
    event_data.destination_status_code = destination_status_code
    event_data.received_bytes = self._GetValueFromStructure(
        structure, 'received_bytes')
    event_data.sent_bytes = self._GetValueFromStructure(structure, 'sent_bytes')
    event_data.request = self._GetValueFromStructure(structure, 'request')
    event_data.user_agent = user_agent
    event_data.ssl_cipher = ssl_cipher
    event_data.ssl_protocol = ssl_protocol
    event_data.destination_group_arn = self._GetValueFromStructure(
        structure, 'destination_group_arn')
    event_data.trace_identifier = self._GetValueFromStructure(
        structure, 'trace_identifier')
    event_data.domain_name = self._GetValueFromStructure(
        structure, 'domain_name')
    event_data.chosen_cert_arn = self._GetValueFromStructure(
        structure, 'chosen_cert_arn')
    event_data.matched_rule_priority = self._GetValueFromStructure(
        structure, 'matched_rule_priority')
    event_data.actions_executed = self._GetValueFromStructure(
        structure, 'actions_executed')
    event_data.redirect_url = redirect_url
    event_data.error_reason = error_reason
    event_data.destination_status_code_list = self._GetValueFromStructure(
        structure, 'destination_status_code_list')
    event_data.classification = classification
    event_data.classification_reason = classification_reason
    event_data.destination_list = destination_list
    event_data.version = self._GetValueFromStructure(structure, 'version')
    event_data.listener = self._GetValueFromStructure(structure, 'listener')
    event_data.connection_duration = self._GetValueFromStructure(
        structure, 'connection_duration')
    event_data.handshake_duration = self._GetValueFromStructure(
        structure, 'handshake_duration')
    event_data.incoming_tls_alert = incoming_tls_alert
    event_data.chosen_cert_serial = chosen_cert_serial
    event_data.tls_named_group = tls_named_group
    event_data.tls_cipher = self._GetValueFromStructure(structure, 'tls_cipher')
    event_data.tls_protocol_version = self._GetValueFromStructure(
        structure, 'tls_protocol_version')
    event_data.alpn_front_end_protocol = self._GetValueFromStructure(
        structure, 'alpn_front_end_protocol')
    event_data.alpn_back_end_protocol = self._GetValueFromStructure(
        structure, 'alpn_back_end_protocol')
    event_data.alpn_client_preference_list = self._GetValueFromStructure(
        structure, 'alpn_client_preference_list')

    response_time_structure = self._GetValueFromStructure(
        structure, 'response_time')
    if response_time_structure:
      event_data.response_time = self._ParseTimeElements(
          response_time_structure)

    request_time_structure = structure.get('request_time')
    if request_time_structure:
      event_data.request_time = self._ParseTimeElements(request_time_structure)

    parser_mediator.ProduceEventData(event_data)

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

    Args:
      time_elements_structure (pyparsing.ParseResults): date and time elements
          of a 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:
      if len(time_elements_structure) == 27:
        date_time = dfdatetime_time_elements.TimeElementsInMicroseconds()
        date_time.CopyFromStringISO8601(time_elements_structure)
      else:
        date_time = dfdatetime_time_elements.TimeElements()
        date_time.CopyFromStringISO8601(time_elements_structure)

        date_time.is_local_time = True

      return date_time

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

  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:
      structure = self._VerifyString(text_reader.lines)
    except errors.ParseError:
      return False

    time_elements_structure = self._GetValueFromStructure(
        structure, 'response_time')

    if time_elements_structure:
      try:
        self._ParseTimeElements(time_elements_structure)
      except errors.ParseError:
        return False

    time_elements_structure = self._GetValueFromStructure(
        structure, 'request_time')

    if time_elements_structure:
      try:
        self._ParseTimeElements(time_elements_structure)
      except errors.ParseError:
        return False

    return True


text_parser.TextLogParser.RegisterPlugin(AWSELBTextPlugin)
