#!/usr/bin/env python
# coding: UTF-8

# TcpdumpWrapper
#
# Copyright(c) 2015-2018 Uptime Technologies, LLC.

from datetime import datetime, timedelta, date, time
import hashlib
import os
import re
import subprocess
import sys

import log


class TcpdumpPacket:
    def __init__(self, ts, src, dst, bytes, debug=None):
        self.ts = self.string2timestamp(ts)
        self.src = src
        self.dst = dst
        self.debug = debug

        self.bytes = bytes

        self.messages = []

        log.debug("+ " + ts + " " + src + " " + dst)

        self.end()

        self.session_id = self.get_session_id()

    def string2timestamp(self, ts):
        t = datetime.strptime(ts, '%Y-%m-%d %H:%M:%S.%f')
        log.debug("ts = " + str(t))
        return t

    def get_session_id(self):
        src_dst = [self.iph_src + ":" + str(self.tcph_src),
                   self.iph_dst + ":" + str(self.tcph_dst)]
        ss = ""
        for s in sorted(src_dst):
            ss = ss + s + " "

        return hashlib.md5(ss).hexdigest()[0:12]

    def get_timestamp(self):
        log.debug("get_timestamp: %s" % str(self.ts))
        return self.ts

    def get_messages(self):
        return self.messages

    def parse_ip_header(self, data):
        self.iph_version = (data[0] >> 4) & 0b1111
        self.iph_header_len = data[0] & 0b1111
        self.iph_tos = data[1]
        self.iph_dgram_len = (data[2] << 8) + data[3]
        self.iph_id = (data[4] << 8) + data[5]
        self.iph_dst = "%d.%d.%d.%d" % (data[12], data[13], data[14], data[15])
        self.iph_src = "%d.%d.%d.%d" % (data[16], data[17], data[18], data[19])

        if self.debug is True:
            print("version  : %d" % self.iph_version)
            print("hd len   : %d (%d)" % (self.iph_header_len,
                                          self.iph_header_len * 4))
            print("tos      : %d" % self.iph_tos)
            print("dgram len: %d" % self.iph_dgram_len)
            print("data  len: %d" % (self.iph_dgram_len -
                                     self.iph_header_len*4))
            print("id       : %d" % self.iph_id)
            print("dst      : %s" % (self.iph_dst))
            print("src      : %s" % (self.iph_src))

        return self.iph_header_len * 4

    def parse_tcp_header(self, data):
        self.tcph_src = (data[0] << 8) + data[1]
        self.tcph_dst = (data[2] << 8) + data[3]
        self.tcph_seq = ((data[4] << 24) + (data[5] << 16) +
                         (data[6] << 8) + data[7])
        self.tcph_offset = (data[12] >> 4) & 0b1111

        if self.debug is True:
            print("src port : %d" % (self.tcph_src))
            print("dst port : %d" % (self.tcph_dst))
            print("seq      : %d" % (self.tcph_seq))
            print("offset   : %d (%d)" % (self.tcph_offset,
                                          self.tcph_offset * 4))

        return self.tcph_offset * 4

    def end(self):
        cur = 0
        iph_len = self.parse_ip_header(self.bytes[cur:])
        cur = cur + iph_len

        tcph_len = self.parse_tcp_header(self.bytes[cur:])
        cur = cur + tcph_len

        self.payload = self.bytes[cur:]

        s = ""
        for d in self.payload:
            s = s + "%02x " % (d)
        log.debug("payload: " + s)

        if len(self.payload) >= 5:
            pos = 0
            cont = True
            while cont:
                if len(self.payload[pos:]) < 5:
                    cont = False
                    break

                ch = self.read_char(self.payload[pos:])
#                if not(ch >= 48 and ch <= 122):
#                    break

                pos = pos + 1

                i = self.read_int32(self.payload[pos:])
                pos = pos + 4

                log.debug("sess: " + self.get_session_id() + ": " +
                          str(self.ts) + ": %c[%x] len=%d" % (ch, ch, i))

                # client to server
                if ch == ord('S'):
                    s = self.read_string(self.payload[pos:], i - 4)
                    self.messages.append([chr(ch), s])
                    log.debug(s)

                elif ch == ord('Q'):
                    s = self.read_string(self.payload[pos:], i - 4)
                    log.debug(s)
                    self.messages.append([chr(ch), s])

                elif ch == ord('P'):
                    s = self.read_string(self.payload[pos:], i - 4)
                    s1 = s.split('\0')
                    log.debug("> " + s1[0] + "," + s1[1])
                    self.messages.append([chr(ch), s1[0], s1[1]])

                elif ch == ord('E'):
                    s = self.read_string(self.payload[pos:], i - 4)
                    self.messages.append([chr(ch), s])

                elif ch == ord('B'):
                    s = self.read_string(self.payload[pos:], i - 4)
                    s1 = s.split('\0')
                    log.debug("> " + s1[0] + "," + s1[1])
                    self.messages.append([chr(ch), s1[0], s1[1]])

                elif ch == ord('X'):
                    self.messages.append([chr(ch), None])
                    cont = False

                # server to client
                elif ch == ord('T'):
                    s = self.read_string(self.payload[pos:], i - 4)
                    self.messages.append([chr(ch), s])
                    log.debug(s)

                elif ch == ord('D'):
                    s = self.read_string(self.payload[pos:], i - 4)
                    self.messages.append([chr(ch), s])
                    log.debug(s)

                elif ch == ord('C'):
                    s = self.read_string(self.payload[pos:], i - 4)
                    self.messages.append([chr(ch), s])
                    log.debug(s)

                elif ch == ord('1'):
                    self.messages.append([chr(ch), None])

                elif ch == ord('2'):
                    self.messages.append([chr(ch), None])

                elif ch == ord('n'):
                    self.messages.append([chr(ch), None])

                elif ch == ord('Z'):
                    self.messages.append([chr(ch), None])
                    cont = False

                pos = pos + (i - 4)

    def parse(self):
        self.pos = 12
        while len(self.payload) > self.pos + 5:
            c = self.read_char()
            log.debug("%02x(%c)" % (c, c))
            l = self.read_int32()
            log.debug(l)
            self.pos = self.pos + l

    def read_char(self, data):
        ch = data[0]
        return ch

    def read_int32(self, data):
        i = (data[0] << 24) + (data[1] << 16) + (data[2] << 8) + (data[3])
        return i

    def read_string(self, data, size):
        s = ""
        i = 0
        while i < size:
            s = s + "%c" % data[i]
            i = i + 1
        return s


class TcpdumpWrapper:
    pkt = None
    tcpdump = None
    process = None
    line = None

    def __init__(self, host=None, port=None, interface=None, inputfile=None,
                 debug=None):
        if debug is True:
            log.setLevel(log.DEBUG)

        self.host = host
        self.port = port
        self.iface = interface
        self.inputfile = inputfile
        self.debug = debug

        if self.port is None:
            self.port = "5432"

        if self.iface is None:
            self.iface = "any"

        self.tcpdump = "tcpdump -tttt"

        if self.inputfile is not None:
            self.tcpdump = self.tcpdump + " -r " + self.inputfile

        self.tcpdump = (self.tcpdump + " -l -i " + self.iface +
                        " -s 0 -X -p tcp port " + str(self.port))

        if self.host is not None:
            self.tcpdump = self.tcpdump + " and host " + self.host

        log.info(self.tcpdump)

        self.process = subprocess.Popen([self.tcpdump],
                                        stdout=subprocess.PIPE,
                                        stderr=subprocess.PIPE,
                                        shell=True,
                                        bufsize=0)

        # header
        self.p1 = re.compile('^([\d-]+) ([\d:\.]+) IP (.*) > (.*): Flags')
        # data
        self.p2 = re.compile('^\t+0x(.+):  (.+)  ')

    def get_packet(self):
        self.data = ""
        self.hdr = None

        while True:
            if self.line is None:
                self.line = self.readline()

                if self.line is None:
                    if self.hdr is not None and self.data is not None:
                        # EOF found.
                        pkt = TcpdumpPacket(self.hdr[0], self.hdr[1],
                                            self.hdr[2],
                                            self.parse_data(self.data),
                                            self.debug)

                        self.data = ""
                        self.hdr = None

                        return pkt
                    else:
                        return None

            if self.line[0] != '\t':
                if len(self.data) > 0:
                    pkt = TcpdumpPacket(self.hdr[0], self.hdr[1], self.hdr[2],
                                        self.parse_data(self.data), self.debug)
                    self.data = ""
                    self.hdr = None
                    return pkt

                self.hdr = self.parse_header(self.line)
                # d: ts, src, dest
                log.debug("Header: ts=" + self.hdr[0] + ", src=" +
                          self.hdr[1] + ", dest=" + self.hdr[2])

            else:
                # log.debug(">" + self.line[10:50] + "<")
                self.data = self.data + self.line[10:50]

            self.line = None

    def get_last_packet(self):
        pkt = None

        if self.hdr is not None and self.data is not None:
            pkt = TcpdumpPacket(self.hdr[0], self.hdr[1], self.hdr[2],
                                self.parse_data(self.data), self.debug)

            self.data = ""
            self.hdr = None

        return pkt

    def readline(self):
        self.process.stdout.flush()
        line = self.process.stdout.readline()

        if len(line) == 0:
            # EOF
            self.process.poll()
            if self.process.returncode != 0:
                log.error("%s" % self.process.stderr.readline())
                sys.exit(1)

            log.debug("readline: EOF")
            return None

        return line.rstrip()

    def parse_header(self, line):
        r1 = self.p1.match(line)

        if r1 is not None:
            # Header line may look like this:
            # 2015-04-30 13:33:27.579311 IP localhost.55641 > \
            # localhost.postgres: Flags [.], ack 66, win 664, \
            # options [nop,nop,TS val 265008484 ecr 265008484], length 0
            ts = r1.group(1) + " " + r1.group(2)
            src = r1.group(3)
            dest = r1.group(4)

            return [ts, src, dest]

        return None

    def parse_data(self, line):
        bytes = []
        # line:
        # 4500 0039 e080 4000 4006 5c3c 7f00 0001
        offset = 0
        length = len(line)

        log.debug("! " + line)
        cur = 0
        while cur < length:
            if line[cur] == ' ':
                cur = cur + 1
                continue
            # chr to hex
            h = int(line[cur:cur+2], 16)
            bytes.append(h)
#            print(payload[cur:cur+2] + ", " + str(h))
            cur = cur + 2

        return bytes

    def parse(self, line):
        # return true when messages avalable.
        msg_avail = False

        log.debug("parse: " + line)

        # packet header info
        r1 = self.p1.match(line)
        if r1 is not None:
            # Header line may look like this:
            # 13:33:27.579311 IP localhost.55641 > localhost.postgres: \
            # Flags [.], ack 66, win 664, options \
            # [nop,nop,TS val 265008484 ecr 265008484], length 0
            log.debug("Header: " + line)
            ts = r1.group(1)
            src = r1.group(2)
            dest = r1.group(3)

            # close the previous packet
            if self.pkt is not None:
                self.pkt.end()

                # retreive all info/messages in the previous packet.
                self.session_id = self.pkt.get_session_id()
                self.timestamp = self.pkt.get_timestamp()
                self.messages = copy.deepcopy(self.pkt.get_messages())
                msg_avail = True

                for m in self.pkt.get_messages():
                    log.debug("ts:%s cmd:%c msg:%s" %
                              (str(self.pkt.get_timestamp()), m[0], m[1]))

            # new packet coming
            self.pkt = TcpdumpPacket(ts, src, dest, self.debug)
            self.packets.append(self.pkt)

        # packet bytes
        r2 = self.p2.match(line)
        if r2 is not None:
            log.debug("matched r2: " + line)

            if self.pkt is not None:
                log.debug("append_bytes: " + line)
                self.pkt.append_bytes(r2.group(2))

        return msg_avail

    def print_packets(self):
        print(self.packets)
