#! /usr/bin/env python
# Script that process stdout from pcf and creates IRI messages
#
# Copyright (C) 2014 Martin Holkovic
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# 
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
# 
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import sys
import time
import os.path
import socket
import getopt

sys.path.append(os.path.join(os.path.dirname(__file__), '../../../'))

from time import gmtime, strftime
from pcapy import *
from modules.sockets.li2 import connectUnixSocket
from modules.tools import signals

socket = None
db = None
maxHash = 16 # pocet unikatnych hodnot hashovacej funkcie
debugMode = 0 # urcuje ci sa budu vypisovat pomocne hlasky
mode = "online" # prepinanie medzi rezimi online a offline

# pomocna funkcia na vypisovanie debugovych hlasok
def debug(string):
    if debugMode != 0:
        unixTime = str(time.time())
        parts = unixTime.split('.')
        if len(parts[1]) == 1:
            parts[1] = parts[1] + "0"
        resultTime = strftime("%d %b %Y %H:%M:%S", time.localtime()) + "." + parts[1]
        
        print >> sys.stderr, resultTime + ": " + string

# funkcia na odoslanie spravy na socket
def send_to_socket(string):
    global socket
    debug("IRI-IIF: " + string)
    
    if mode == "online": # pri rezime offline sa neposielaju na IRI-IIF spravy
        socket.send(string + "\n")


################################################################################
#### Funkcie na pracu s ukladanymi datami ######################################
################################################################################

# kalkulacia indexu do pola zo stringu, rozsah 0-maxHash
def hashIndex(string):
    global maxHash
    summary = 0
    for i in range(0, len(string)): # prejde kazdym znakom v stringu
        num = ord(string[i]) # zistime si ASCII hodnotu znaku
        summary = summary + num # ciselnu hodnotu pricitame k sume
        
    summary = summary%maxHash # hodnota indexu je v rozsahu 0-maxHash
    return summary

# vytvorenie 2 rozmerneho pola pre ukladanie dat, velkost maxHash polozek
def dbCreate():
    global maxHash
    db = [] # prazdne pole
    for i in range(0, maxHash): # vytvori zadana N-rozmerne pole
        db.append([]) # vytvori prazdnu polozku
    return db

# vyhladanie zaznamu v db
def dbSearchSession(db, session):
    index = hashIndex(session) # vypocet indexu
    
    for elem in db[index]: # hledame polozku v zezname (podla session ID)
        if elem[0] == session: # nasli sme polozku
            return elem # vratim hladanu polozku
    
    return None # polozka sa nenasla

# vlozenie polozky do db
def dbInsert(db, session, mac):
    elem = dbSearchSession(db, session) # zistenie ci uz dana polozka existuje
    
    lastStatus = 0
    if elem != None: #polozka uz existuje, zistim aktualny status
        lastStatus = elem[1]
    
    if lastStatus == 5: #session este existuje, ukonci sa
        dbDelete(db, session) # odstranenie polozky z db
    
    if elem == None or lastStatus != 5: #nevytvoreny session
        index = hashIndex(session) # vypocet indexu
        elem = [session, 1, mac, "", []] # vytvorenie novej polozky
        db[index].append(elem) # pridanie do db
        send_to_socket("('pppoe', "+str(time.time())+", 'BEGIN','Client made connection with BRAS',[('PPP SESSION','"+session+"'),('MAC','"+mac+"')])") #MAC adresa '"+mac+"' naviazala spojenie cislo "+session
        return db

# pridanie mena uzivatela k PPP relacii
def dbUpdateName(db, session, mac, user):
    elem = dbSearchSession(db, session) # vyhladanie danej polozky
    
    lastStatus = 0
    if elem == None: # session este neni vytvoreny
        dbInsert(db, session, 1, mac)
        lastStatus = 1
    
    #aktualny status je PPPoE D alebo neni ulozene meno
    if lastStatus == 1 or elem[1] == 1 or elem[3] == "": 
        index = hashIndex(session) # vypocet indexu
        for i in range(0, len(db[index])): # prejdem kazdu polozku
            if db[index][i][0] == session: # nasiel som hladanu relaciu
                if db[index][i][3] == "": # username doteraz nebol znamy
                    db[index][i][3] = user # k session pridam username
                    if db[index][i][1] >= 3: # vypise sa len ked sa uz neocakava Ack
                        # vykona sa len pri prehodenych paketoch
                        send_to_socket("('pppoe', "+str(time.time())+", 'BEGIN', 'Find user name of user', [('PPP SESSION','"+session+"'),('MAC','"+db[index][i][2]+"'),('PPP LOGIN','"+user+"')])") #Na spojeni cislo '"+session+"' je autentizovany uzivatel '"+user+"'"
                    else:
                        send_to_socket("('pppoe', "+str(time.time())+", 'REPORT', 'User is trying to authenticate', [('PPP SESSION','"+session+"'),('MAC','"+db[index][i][2]+"'),('PPP LOGIN','"+user+"')])") #Na spojeni cislo '"+session+"' sa pokusa autentizovat uzivatel '"+user+"'"
                if db[index][i][1] == 1: # v pripade statusu PPPoe D updatnem status
                    db[index][i][1] = 2 # zmenim aktualny stav na auth1
        return db

# odstranenie zaznamu z db
def dbDelete(db, session): 
    index = hashIndex(session) # vypocet indexu
    for i in range(0, len(db[index])): # prejdem kazdu polozku
        if db[index][i][0] == session: # nasiel som hladanu relaciu
            #dany session -> zmazat IPCP adresu
            for ip in db[index][i][4]: #prejdem kazdu IP
                send_to_socket("('pppoe', "+str(time.time())+", 'END', 'Validity of IP address is expired', [('PPP SESSION','"+session+"'),('IP','"+ip+"')])") #IP adrese "+ip+" skoncila platnost"
            
            if db[index][i][3] != "": # bolo detekovane uzivatelske meno
                send_to_socket("('pppoe', "+str(time.time())+", 'END', 'Connection was terminated', [('PPP SESSION','"+session+"'),('MAC','"+db[index][i][2]+"'),('PPP LOGIN','"+db[index][i][3]+"')])") #Spojenie "+session+" bolo zrusene"
            else:
                send_to_socket("('pppoe', "+str(time.time())+", 'END', 'Connection was terminated', [('PPP SESSION','"+session+"'),('MAC','"+db[index][i][2]+"')])") #Spojenie "+session+" bolo zrusene"
            
            db[index].remove(db[index][i]) # odstraneni celeho zaznamu
    #print "IRI-report:Spojenie cislo '"+session+"' bolo ukoncene"
    return db

# potvrdenie vytvorenie relacie
def dbConfirmAuth(db, session, mac):
    elem = dbSearchSession(db, session) # vyhladanie relacie
    
    lastStatus = 0
    if elem == None: # session este neni vytvoreny
        dbInsert(db, session, 1, mac)
        lastStatus = 1
    
    if lastStatus == 1 or elem[1] <= 2: #aktualny status je PPPoE D alebo auth1
        index = hashIndex(session) # vypocet indexu
        for i in range(0, len(db[index])): # prejdem kazdu polozku
            if db[index][i][0] == session: # nasiel som danu relaciu
                db[index][i][1] = 3 # zmenim aktualny stav na auth2
                if db[index][i][3] != "": # session ma ulozene username
                    send_to_socket("('pppoe', "+str(time.time())+", 'BEGIN', 'Find user name of user', [('PPP SESSION','"+session+"'),('MAC','"+elem[2]+"'),('PPP LOGIN','"+elem[3]+"')])") #Na spojeni cislo '"+session+"' je uzivatel '"+elem[3]+"'"
        return db


# vyhladanie relacie ktora obsahuje zadanu IP adresu
def dbSearchIp(db, ip):
    global maxHash
    for i in range(0, maxHash): #kazdy index
        for j in range(0, len(db[i])): #kazda polozka v indexe
            for k in range(0, len(db[i][j][4])): #kazda IP v polozke
                if db[i][j][4][k] == ip: # nasiel som danu adresu
                    return db[i][j]
    return None

# pridanie IP adresy k naviazanemu PPP spojeniu
def dbAddIp(db, session, mac, ip):
    if dbSearchSession(db, session) == None: # ked session neexistuje, vytvorim ho
        dbInsert(db, session, mac)
    
    index = hashIndex(session) # vypocet indexu
    for i in range(0,len(db[index])): # prejdem kazdy prvek s rovnakym indexem
        if db[index][i][0] == session: # nasel sem hladanu relaciu
            db[index][i][4].append(ip) # pridanie IP adresy
            db[index][i][1] = 4 # zmenim stav na IPCP
            send_to_socket("('pppoe', "+str(time.time())+", 'BEGIN', 'IP address was assigned to user', [('PPP SESSION','"+session+"'),('IP','"+ip+"')])")
    return db


################################################################################
#### Funkcie na prekonvertovanie dat do inej zobrazovacej podoby ###############
################################################################################

#funkcia na vypocet ciselnej hodnoty zo stringu
def str_to_int(string):
    result = 0
    for char in string: # prejdem kazdy znak zo vstupu
        result = result*256 + ord(char) # vysledok 
    return result

#string prekonvertujem po bajtoch do hexa podoby
def str_to_hex(string):
    hexResult = ""
    for char in string: # prejdem kazdy znak zo vstupu
        hexa = hex(ord(char)).replace('0x', '') # ziskam hexa podobu a odstranim znak "0x"
        if len(hexa) == 1: #kazdy bajt musi mat 2 zobrazene znaky
            hexResult += "0"
        hexResult += hexa
    return hexResult

#cislo prekonvertujem do hexa podoby !!! funguje len do 16 bitov(2 bajty) z dovodu konstanty 65520 !!!
def int_to_hex(num):
    hexResult = ""
    if num == 0: # specialny pripad
        hexResult = "0"
    while num > 0:
        rest = num % 16
        if rest > 9: #znak A-F
            char = chr(rest + 97 - 10)
        else: # cislice 0-9
            char = chr(rest + 48)
        hexResult = char + hexResult
        num = num & 65520 #vynulujem dolne 4 bity
        num = num / 16 # posuv o 4 bity do prava
    return hexResult

################################################################################
#### Funkcie na prekonvertovanie surovych dat do ludskej podoby ################
################################################################################

#prekonvertuje string do podoby MAC adresy
# MAC adresa musi byt dlha 12 znakov (t.j. zadana bez oddelovacov ":")
def str_to_mac(string):
    result = ""
    for i in range(0, 12): #prejde postupne 12 znakov
        result += string[i]
        if i%2 == 1 and i != 11: #medzi pary bajtov vlozi ":"
            result += ":"
    return result


#prekonvertuje string do podoby IPv4 adresy
# IPv4 adresa musi byt zadana v podobe 4 bajtov
def str_to_ip4(string):
    ip = ""
    for i in range(0,4): # prejde postupne 4 bajty
        ip += str(str_to_int(string[i:i+1])) #bajt prevedie na cislo a vytvori z neho string
        if i != 3: # medzi jednotlive casti sa vlozi oddelovac "."
            ip += "."
    return ip

#prekonvertuje string do IPv6 podoby
def str_to_ip6(string):
    ip = ""
    for i in range(0,len(string),2):
        if i != 0:
            ip += ":"
        num = str_to_int(string[i:i+2])
        ip += int_to_hex(num)
    # v adrese nahradim najvacsi vyskyt :0:0: za ::
    maxi = len(string)/2
    for i in range(3, maxi):
        x = maxi - i + 3
        temp = ""
        for j in range(0,x):
            temp += ":0"
        temp2 = temp + ":"
        if temp2 in ip: # :0:0: ma vacsiu prioritu ako :0:0
            ip = ip.replace(temp2, "::", 1)
            return ip #aby sa nenahradzovalo viac ako 1x
        if temp in ip:
            ip = ip.replace(temp, "::", 1)
            return ip #aby sa nenahradzovalo viac ako 1x
    return ip





################################################################################
#### Funkcie na pracu s ethernem datami ########################################
################################################################################

# funkcia vrati zo vstupnych dat ethernet hlavicku
def ether_header(data):
    #etherType = str_to_hex(data[12:14])
    # nepodporuje etherType = 0 (velkost headru je 0)
    # pri nahravani cez any nemoze byt VLAN ID 1757 s priority 4
    # pri nahravani cez any musi za L2 byt hned IPv6 header
    if str_to_hex(data[12:14]) == "8100": # 802.1Q VLAN tag na beznom rozhrani (napr. eth0)
        return data[0:18]
    elif str_to_hex(data[12:14]) == "0000": # nahrate cez interface any
        if str_to_hex(data[14:16]) == "8863" or str_to_hex(data[14:16]) == "8864": # bez VLAN
            return data[0:16]
        else: # s VLAN
            return data[0:18]
    else: # etherType
        return data[0:14]

# funkcia vrati z ethernet hlavicky zapuzdrene data
def ether_data(data, headerLength):
    return data[headerLength:len(data)]

# funkcia vrati z ethernet hlavicky zdrojovu MAC adresu
def source_mac(etherHeader):
    return str_to_mac(str_to_hex(etherHeader[6:12]))

# funkcia vrati z ethernet hlavicky typ zapuzdreneho protokolu
def ether_type(etherHeader):
    length = len(etherHeader)
    return str_to_hex(etherHeader[length-2:length])





# funkcia vrati z ethernet hlavicky cielovu MAC adresu
def dest_mac(etherHeader):
    return str_to_mac(str_to_hex(etherHeader[0:6]))



################################################################################
#### Funkcie na pracu s PPPoE Discovery datami #################################
################################################################################

# funkcia vrati z PPPoE Discovery hlavicky hodnotu code
def pppoed_code(pppoedHeader):
    return str_to_hex(pppoedHeader[1:2])

# funkcia vrati z PPPoE Discovery hlavicky session ID
def pppoed_session(pppoedHeader):
    return str_to_hex(pppoedHeader[2:4])

################################################################################
#### Funkcie na pracu s PPPoE Session datami ###################################
################################################################################

# funkcia vrati z ethernetovych dat typu PPPoE: PPPoE Session + PPP hlavicku
def pppoe_header(pppoe):
    return pppoe[0:8]

# funkcia vrati z PPPoE Session hlavicky session ID
def pppoe_session(pppoeHeader):
    return str_to_hex(pppoeHeader[2:4])

# funkcia vrati z PPPoE Session hlavicky typ zapuzdreneho protokolu
def pppoe_protocol(pppoeHeader):
    return str_to_hex(pppoeHeader[6:8])

# funkcia vrati z ethernetovych dat typu PPPoE: zapuzdreny protokol
def pppoe_data(pppoe):
    payloadLength = str_to_int(pppoe[4:6])
    return pppoe[8:8+payloadLength-2]


################################################################################
#### Funkcie na pracu s CHAP datami ############################################
################################################################################

# funkcia vrati z datovej casti PPPoE typu CHAP, hodnotu code
def chap_code(chapData):
    return str_to_hex(chapData[0:1])

# funkcia vrati z datovej casti PPPoE typu CHAP prihlasovacie meno
def chap_name(chapData):
    length = str_to_int(chapData[2:4])
    valueSize = str_to_int(chapData[4:5])
    return chapData[5+valueSize:length]


################################################################################
#### Funkcie na pracu s PAP datami #############################################
################################################################################

# funkcia vrati z datovej casti PPPoE typu PAP, hodnotu code
def pap_code(papData):
    return str_to_hex(papData[0:1])

# funkcia vrati z datovej casti PPPoE typu PAP prihlasovacie meno
def pap_name(papData):
    nameLength = str_to_int(papData[4:5])
    return papData[5:5+nameLength]


################################################################################
#### Funkcie na pracu s IP(v6)CP datami ############################################
################################################################################

# funkcia vrati z datovej casti PPPoE typu IPCP, hodnotu code
def ipcp_code(ipcpData):
    return str_to_hex(ipcpData[0:1])

# funkcia vrati z datovej casti PPPoE typu IPCP pridelenu IPv4 adresu
def ipcp_ip(ipcpData):
    length = str_to_int(ipcpData[2:4]) #velkost options dat
    offset = 4
    while offset < length: #pokial je offset v ramci options dat
        if str_to_hex(ipcpData[offset]) == "03": #parameter IP adresa
            return str_to_ip4(ipcpData[offset+2:offset+6])
        else: #posunem sa na dalsi parameter
            offset += str_to_int(ipcpData[offset+1])
    return -1 #IP adresa sa v options nenachadza

# funkcia vrati z interface identifier lokalnu IPv6 adresu
def ipv6ip_from_intId(string):
    ip = ""
    for i in range(0,len(string),2):
        if i != 0: # medzi bloky IPv6 adresy vlozim oddelovac ":"
            ip += ":"
        num = str_to_int(string[i:i+2])
        ip += int_to_hex(num) # pridanie do stringu
    ip = "fe80::"+ip
    return ip

# funkcia vrati z datovej casti PPPoE typu IPV6CP pridelenu IPv6 adresu
def ipv6cp_ip(ipcpData):
    length = str_to_int(ipcpData[2:4]) #velkost options dat
    offset = 4
    while offset < length: #pokial je offset v ramci options dat
        if str_to_hex(ipcpData[offset]) == "01": #parameter IPv6 adresa
            interfaceId = ipcpData[offset+2:offset+10]
            return ipv6ip_from_intId(interfaceId)
        else: #posunem sa na dalsi parameter
            offset += str_to_int(ipcpData[offset+1])
    return -1 #IP adresa sa v options nenachadza





################################################################################
#### Hlavna funkcia ktora spracovava kazdy prijaty frame #######################
################################################################################

#funkcia spracovavajuca vsetky prijate data zo siete
def callback(hdr, data):
    global db
    
    etherHeader = ether_header(data) # zistenie ethernet hlavicky
    if ether_type(etherHeader) == "8863": #PPPoE Discovery
        pppoedHeader = ether_data(data, len(etherHeader)) # zistenie ethernet dat
################################################################################
#### PPPoE Discovery Active Discovery Session-confirmation #####################
################################################################################ 
        if pppoed_code(pppoedHeader) == "65": #PADS
            pppoedSession = pppoed_session(pppoedHeader) # zistenie session ID
            destMac = dest_mac(etherHeader)
            db = dbInsert(db, pppoedSession, destMac)
            
################################################################################
#### PPPoE Discovery Active Discovery Session-termination ######################
################################################################################ 
        if pppoed_code(pppoedHeader) == "a7": #PADT
            pppoedSession = pppoed_session(pppoedHeader) # zistenie session ID
            elem = dbSearchSession(db, pppoedSession)
            if elem != None: #dany session este existuje
                db = dbDelete(db, pppoedSession)
    
################################################################################
    if ether_type(etherHeader) == "8864": #PPPoE Session
        pppoe = ether_data(data, len(etherHeader))
        pppoeHeader = pppoe_header(pppoe)
################################################################################       
#### Challenge Handshake Authentication Protocol Response & Success ############
################################################################################
        if pppoe_protocol(pppoeHeader) == "c223": #CHAP
            chapData = pppoe_data(pppoe)
            pppoeSession = pppoe_session(pppoeHeader) # zistenie session ID
            sourceMac = source_mac(etherHeader)
            if chap_code(chapData) == "02": #Response
                chapName = chap_name(chapData) # zistenie prihlasovacieho mena
                db = dbUpdateName(db, pppoeSession, sourceMac, chapName)
            elif chap_code(chapData) == "03": #Success
                db = dbConfirmAuth(db, pppoeSession, sourceMac)
        
################################################################################
#### Password Authentication Protocol Request ##################################
################################################################################
        if pppoe_protocol(pppoeHeader) == "c023": #PAP
            papData = pppoe_data(pppoe)
            pppoeSession = pppoe_session(pppoe)
            sourceMac = source_mac(etherHeader)
            if pap_code(papData) == "01": #Request
                papName = pap_name(papData)
                db = dbUpdateName(db, pppoeSession, sourceMac, papName)
            elif pap_code(papData) == "02": #Ack
                db = dbConfirmAuth(db, pppoeSession, sourceMac)
        
################################################################################
#### IP Control Protocol Configuration Ack #####################################
################################################################################
        if pppoe_protocol(pppoeHeader) == '8021': #IPCP
            ipcpData = pppoe_data(pppoe)
            if ipcp_code(ipcpData) == '02': #Configuration Ack
                pppoeSession = pppoe_session(pppoe)
                element = dbSearchSession(db, pppoeSession)
                destMac = dest_mac(etherHeader)
                if element != None and element[2] == destMac: # server potvrdzuje IP adresu klienta
                    ipcpIp = ipcp_ip(ipcpData) #najdenie IP adresy v options
                    
                    if ipcpIp != -1: #IP adresa bola v IPCP sprave najdena
                        elem = dbSearchIp(db, ipcpIp)
                        if elem != None and elem[0] != pppoeSession: # IP bola priradena inej session
                            db = dbDelete(db, elem[0]) # odstranim povodny session
                            db = dbAddIp(db, pppoeSession, destMac, ipcpIp)
                        elif elem == None: # IP este nie je priradena
                            db = dbAddIp(db, pppoeSession, destMac, ipcpIp)
        
################################################################################
#### IPv6 Control Protocol Configuration Ack ###################################
################################################################################
        if pppoe_protocol(pppoeHeader) == '8057': #IPV6CP
            ipcpData = pppoe_data(pppoe)
            if ipcp_code(ipcpData) == '02': #Configuration Ack
                pppoeSession = pppoe_session(pppoe)
                element = dbSearchSession(db, pppoeSession)
                destMac = dest_mac(etherHeader)
                if element != None and element[2] == destMac: # server potvrdzuje IP adresu klienta
                    ipv6cpIp = ipv6cp_ip(ipcpData) #najdenie IP adresy v options
                    
                    if ipv6cpIp != -1: #IP adresa bola v IPV6CP najdena
                        elem = dbSearchIp(db, ipv6cpIp)
                        if elem != None and elem[0] != pppoeSession: # IP bola priradena inej session
                            db = dbDelete(db, elem[0]) # odstranim povodny session
                            db = dbAddIp(db, pppoeSession, destMac, ipv6cpIp)
                        elif elem == None: # IP este nie je priradena
                            db = dbAddIp(db, pppoeSession, destMac, ipv6cpIp)
               
################################################################################
#### Funkcia main() ############################################################
################################################################################

def main(argv):
    global db
    global socket
    global debugMode
    global mode
    
    if os.geteuid() != 0:
        sys.stderr.write("You must be root to run this script.\n")
        exit()
    
    db = dbCreate() #inicializovanie ulozneho priestoru
    
    string = "any"
    debugMode = 0
    readFrom = "socket"
    
    try:
        opts, args = getopt.getopt(argv, "i:p:vo", ["interface=","pcap=","verbose","offline"]) # spracovanie parametrov
    except getopt.GetoptError, err:
        sys.stderr.write(str(err) + "\n")
        sys.exit(2)
    
    for param, value in opts: # prejdenie parametrov
        if param in("-i, --interface"):
            string = value
        
        elif param in ("-p,--pcap"):
            string = value
            readFrom = "file"
        
        elif param in ("-v,--verbose"):
            debugMode = 1
        
        elif param in ("-o,--offline"):
            mode = "offline"
    
    
    if mode == "online":
        socket = connectUnixSocket("/tmp/iricol")
    
    if readFrom == "file":
        reader = open_offline(string) # zachytavanie zo suboru
    else:
        try:
            reader = open_live(string, 1500, 0, 100) # zive zachytavanie
        except Exception, e:
            sys.stderr.write("Cannot open device %s: %s\n" % (string, str(e)))
            reader = None
    
    if reader:
        reader.setfilter("")
        reader.loop(0, callback)


if __name__ == "__main__":
    
    signals.setHandlerForCommonSignals(signals.signalExceptionHandler)
    try:
        main(sys.argv[1:])
    except Exception, e:
        print "PPPoE: Ukonceni na vyjimce - " + str(e)

