###########################################################################
# guidedogdoc.py - description                                            #
# ------------------------------                                          #
# begin     : Wed Oct 11 2006                                             #
# copyright : (C) 2006 by Simon Edwards                                   #
# email     : simon@simonzone.com                                         #
#                                                                         #
###########################################################################
#                                                                         #
#   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 2 of the License, or     #
#   (at your option) any later version.                                   #
#                                                                         #
###########################################################################

from PyQt4.QtCore import *
import re
from PyKDE4.kdecore import *
from sealed import *

############################################################################
class IPRange(object):
    INVALID = 0
    LOCAL_MACHINE = 1
    DOMAIN_NAME = 2
    IP = 3
    IP_RANGE = 4

    @sealed
    def __init__(self,a=""):
        self.gottype = False
        self.type = self.INVALID
        self.address = a
        
    def setAddress(self,a):
        self.address = a
        self.gottype = False
        
    def getAddress(self):
        return self.address
        
    def getType(self):
        if not self.gottype:
            self.type = self.__guessType()
            self.gottype = True
    
        return self.type
    
    def __guessType(self):
        sanity = re.compile("^[0-9a-zA-Z./-]*$")
        domainnametest = re.compile("^([a-zA-Z][a-zA-Z0-9-]+\\.)+[a-zA-Z][a-zA-Z0-9-]+$")
        iptest = re.compile("^([0-9]+)\\.([0-9]+)\\.([0-9]+)\\.([0-9]+)$")
        ipmaskedtest = re.compile("^([0-9]+)\\.([0-9]+)\\.([0-9]+)\\.([0-9]+)/([0-9]+)$")
        ipmasked2test = re.compile("^([0-9]+)\\.([0-9]+)\\.([0-9]+)\\.([0-9]+)/([0-9]+)\\.([0-9]+)\\.([0-9]+)\\.([0-9]+)$")
        
        # Smoke text
        if len(self.address)==0:
            return self.INVALID
        
        if sanity.match(self.address) is None:
            return self.INVALID
    
        # Test against the domainname regexp.
        if domainnametest.match(self.address) is not None:
            return self.DOMAIN_NAME
        
        # Ok, now lets try the IP address regexp.
        iptestmatch = iptest.match(self.address)
        if iptestmatch is not None:
            try:
                ipbyte = int(iptestmatch.group(1))
                if ipbyte<0 or ipbyte>255:
                    return self.INVALID
                
                ipbyte = int(iptestmatch.group(2))
                if ipbyte<0 or ipbyte>255:
                    return self.INVALID
                
                ipbyte = int(iptestmatch.group(3))
                if ipbyte<0 or ipbyte>255:
                    return self.INVALID
                
                ipbyte = int(iptestmatch.group(4))
                if ipbyte<0 or ipbyte>255:
                    return self.INVALID
                
                return self.IP
            except ValueError:
                return self.INVALID
    
        # Now lets try the IP address regexp.
        ipmaskedtestmatch = ipmaskedtest.match(self.address)
        if ipmaskedtestmatch is not None:
            try:
                ipbyte = int(ipmaskedtestmatch.group(1))
                if ipbyte<0 or ipbyte>255:
                    return self.INVALID
                
                ipbyte = int(ipmaskedtestmatch.group(2))
                if ipbyte<0 or ipbyte>255:
                    return self.INVALID
                
                ipbyte = int(ipmaskedtestmatch.group(3))
                if ipbyte<0 or ipbyte>255:
                    return self.INVALID
                
                ipbyte = int(ipmaskedtestmatch.group(4))
                if ipbyte<0 or ipbyte>255:
                    return self.INVALID
                
                ipbyte = int(ipmaskedtestmatch.group(5))
                if ipbyte<0 or ipbyte>32:
                    return self.INVALID
                return self.IP_RANGE

            except ValueError:
                return self.INVALID
        
        ipmasked2testmatch = ipmasked2test.match(self.address)
        if ipmasked2testmatch is not None:
            try:
                ipbyte = int(ipmasked2testmatch.group(1))
                if ipbyte<0 or ipbyte>255:
                    return self.INVALID
                
                ipbyte = int(ipmasked2testmatch.group(2))
                if ipbyte<0 or ipbyte>255:
                    return self.INVALID
                
                ipbyte = int(ipmasked2testmatch.group(3))
                if ipbyte<0 or ipbyte>255:
                    return self.INVALID
                
                ipbyte = int(ipmasked2testmatch.group(4))
                if ipbyte<0 or ipbyte>255:
                    return INVALID
                
                ipbyte = int(ipmasked2testmatch.group(5))
                if ipbyte<0 or ipbyte>255:
                    return self.INVALID
                
                ipbyte = int(ipmasked2testmatch.group(6))
                if ipbyte<0 or ipbyte>255:
                    return INVALID
                
                ipbyte = int(ipmasked2testmatch.group(7))
                if ipbyte<0 or ipbyte>255:
                    return self.INVALID
                
                ipbyte = int(ipmasked2testmatch.group(8))
                if ipbyte<0 or ipbyte>255:
                    return self.INVALID
                
                return self.IP_RANGE
            except ValueError:
                return self.INVALID
        
        return self.INVALID
    
############################################################################
class PortForwardRule(object):
    IPPROTO_TCP = 0
    IPPROTO_UDP = 1
    
    @sealed
    def __init__(self):
        self.specifyoriginal = False
        self.originaladdress = IPRange()
        
        self.specifynewaddress = False
        self.newaddress = IPRange()
        self.iptype = self.IPPROTO_TCP
        self.originalport = 0
        self.newport = 0
        self.comment = ""
    
    def getSummary(self):

        summary = ""
    
        if self.specifyoriginal:
            summary += self.originaladdress.getAddress()
            summary += ":"
    
        if self.iptype==self.IPPROTO_TCP:
            summary += "TCP "
        else:
            summary += "UDP "
        
        summary += str(self.originalport)
        summary += " -> "
    
        if self.specifynewaddress:
            summary += self.newaddress.getAddress()
            summary += ":"
        
        summary += str(self.newport)
        summary += " "
        summary += self.comment
        
        return summary

############################################################################
class GuidedogDoc(object):
    @sealed
    def __init__(self):
        self.description = ""

        # list of PortForwardRule
        self.forwardrulelist = []
        
        # list of IPRange
        self.nomasqueradelist = []
    
        self.disabled = False
        self.routing = False
        self.masquerade = False
        self.masqueradeftp = False
        self.masqueradeirc = False

        self.factoryDefaults()
        
    def setDescription(self,description):
        self.description = description
        
    def getDescription(self):
        return self.description
    
    def setDisabled(self,on):
        self.disabled = on;
        
    def isDisabled(self):
        return self.disabled
        
    def setRouting(self,on):
        self.routing = on
        
    def isRouting(self):
        return self.routing
        
    def setMasquerade(self,on):
        self.masquerade = on

    def isMasquerade(self):
        return self.masquerade

    def setMasqueradeFTP(self, on):
        self.masqueradeftp = on

    def isMasqueradeFTP(self):
        return self.masqueradeftp

    def setMasqueradeIRC(self,on):
        self.masqueradeirc = on

    def isMasqueradeIRC(self):
        return self.masqueradeirc

    def factoryDefaults(self):
        self.disabled = False
        self.routing = False
        self.masquerade = False
        self.masqueradeftp = False
        self.masqueradeirc = False
        self.description = ""

    def writeScript(self,stream):
        stream.setCodec(QTextCodec.codecForName("ISO 8859-1"))

        stream << r"""#!/bin/sh
# [Guidedog]
# DO NOT EDIT!
# This script was generated by "Guidedog" by Simon Edwards
# http://www.simonzone.com/software/guidedog/ This script requires Linux
# kernel 2.4.x and iptables.
#
# [Description]
"""
        for line in self.description.split('\n'):
            stream << "# " << line << "\n"
            
        stream << """# [Config]
# DISABLED=""" << ["0","1"][self.disabled] << """
# ROUTING=""" << ["0","1"][self.routing] << """
# MASQUERADE=""" << ["0","1"][self.masquerade] << """
# MASQUERADEFTP=""" << ["0","1"][self.masqueradeftp] << """
# MASQUERADEIRC=""" << ["0","1"][self.masqueradeirc] << """
"""
        for item in self.nomasqueradelist:
            stream << "# NOMASQUERADEADDRESS=" << item.getAddress() << "\n"
            
        for rule in self.forwardrulelist:
            stream << """# [Forwarding]
# TYPE=""" << ["UDP","TCP"][rule.iptype==PortForwardRule.IPPROTO_TCP] << """
# ORIGINALPORT=""" << str(rule.originalport) << """
# SPECIFYORIGINALADDRESS=""" << ["0","1"][rule.specifyoriginal] << """
# ORIGINALADDRESS=""" << rule.originaladdress.getAddress() << """
# NEWPORT=""" << str(rule.newport) << """
# SPECIFYNEWADDRESS=""" << ["0","1"][rule.specifynewaddress] << """
# NEWADDRESS=""" << rule.newaddress.getAddress() << """
# COMMENT=""" << rule.comment << """
"""
        stream << """# [End]

# Real code starts here
# If you change the line below then also change the # DISABLED line above.
DISABLE_GUIDEDOG=""" << ["0","1"][self.disabled] << """
if [ $DISABLE_GUIDEDOG -eq 0 ]; then
# Set the path
PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/sbin
if test -z $GUIDEDOG_VERBOSE; then
  GUIDEDOG_VERBOSE=0
fi;
logger -p auth.info -t guidedog Configuring routing now.
[ $GUIDEDOG_VERBOSE -eq 1 ] && echo "Resetting routing rules."
# Shut down routing
echo 0 > /proc/sys/net/ipv4/ip_forward 2> /dev/null
iptables -t nat -P PREROUTING DROP
iptables -t nat -P POSTROUTING DROP
iptables -t nat -P OUTPUT DROP
iptables -t mangle -P PREROUTING DROP
iptables -t mangle -P POSTROUTING DROP
# Delete any existing chains
iptables -t nat -F
iptables -t nat -X
iptables -t mangle -F
iptables -t mangle -X

# Enable/disable routing
"""
        if self.routing:
            stream << "[ $GUIDEDOG_VERBOSE -eq 1 ] && echo \"" << i18n("Enabling routing.") << "\"\n"
        else:
            stream << "[ $GUIDEDOG_VERBOSE -eq 1 ] && echo \"" << i18n("Disabling routing.") << "\"\n"
    
        # Code to enable/disable routing.
        stream << """echo """ << ["0","1"][self.routing] << """ > /proc/sys/net/ipv4/ip_forward 2> /dev/null

[ $GUIDEDOG_VERBOSE -eq 1 ] && echo "Loading kernel modules."
modprobe iptable_nat
"""
        if self.routing and self.masquerade:
            if self.masqueradeftp:
                stream << "modprobe ip_nat_ftp\n"
            
            if self.masqueradeirc:
                stream << "modprobe ip_nat_irc\n"

        stream << """# Port forwarding/Tunnelling
iptables -t nat -N gforward
iptables -t nat -N ftolocal
iptables -t mangle -N gforward
iptables -t mangle -N ftolocal

# Switch the current language for a moment
GUIDEDOG_BACKUP_LANG=$LANG
GUIDEDOG_BACKUP_LC_ALL=$LC_ALL
LANG=US
LC_ALL=US
export LANG
export LC_ALL
# Work out our local IPs.
LOCAL_IP="`ifconfig | gawk '/inet addr:/ { match(\$0,/inet addr:[[:digit:]\\\\.]+/)
printf \\"%s\\\\n\\",substr(\\$0,RSTART+10,RLENGTH-10) }'`"
# Restore the language setting
LANG=$GUIDEDOG_BACKUP_LANG
LC_ALL=$GUIDEDOG_BACKUP_LC_ALL
export LANG
export LC_ALL
for X in $LOCAL_IP ; do
    iptables -t nat -A gforward -d $X -j ftolocal
    iptables -t mangle -A gforward -d $X -j ftolocal
done
"""
        for rule in self.forwardrulelist:
            if rule.specifyoriginal:
                stream << "# " << rule.comment <<"\n"
                if rule.specifynewaddress:
                    stream << "iptables -t mangle -A gforward " \
                    "-p " << ["udp","tcp"][rule.iptype==PortForwardRule.IPPROTO_TCP] << " " \
                    "--dport " << str(rule.originalport) << " " \
                    "-d " << rule.originaladdress.getAddress() << " " \
                    "-j MARK --set-mark 1\n"
                
                stream << "iptables -t nat -A gforward " \
                    "-p " << ["udp","tcp"][rule.iptype==PortForwardRule.IPPROTO_TCP] << " " \
                    "--dport " << str(rule.originalport) <<" " \
                    "-d " << rule.originaladdress.getAddress() << " "
                if rule.specifynewaddress:
                    stream << "-j DNAT --to-destination " << rule.newaddress.getAddress() << ":"
                else:
                    # Redirecting to the local machine.
                    stream << "-j REDIRECT --to-ports "
                
                stream << str(rule.newport) <<"\n"

        for rule in self.forwardrulelist:
            if not rule.specifyoriginal:
                stream << "# " << rule.comment << "\n"
                if rule.specifynewaddress:
                    stream << "iptables -t mangle -A ftolocal " \
                        "-p " << ["udp","tcp"][rule.iptype==PortForwardRule.IPPROTO_TCP] <<" " \
                        "--dport " << str(rule.originalport) << " " \
                        "-j MARK --set-mark 1\n"
    
                stream << "iptables -t nat -A ftolocal " \
                    "-p " << ["udp","tcp"][rule.iptype==PortForwardRule.IPPROTO_TCP] <<" " \
                    "--dport " << str(rule.originalport) << " "
                if rule.specifynewaddress:
                    stream << "-j DNAT --to-destination " << rule.newaddress.getAddress() << ":"
                else:
                    # Redirecting to the local machine.
                    stream << "-j REDIRECT --to-ports "
                stream << str(rule.newport) << "\n"

        stream << """iptables -t nat -A PREROUTING -j gforward
iptables -t nat -A OUTPUT -j gforward
iptables -t mangle -A PREROUTING -j gforward
# Marked packets are in the process of being port forwarded.
# We also need to source Nat them too.
iptables -t nat -A POSTROUTING -m mark --mark 1 -j MASQUERADE
"""

        # Code to enable/disable masquerade.
        if self.routing and self.masquerade:
            # Fill up the NAT table with rules.
            stream << """[ $GUIDEDOG_VERBOSE -eq 1 ] && echo "Inserting masquerade rules."
iptables -t nat -N fromprivate
# Packets from the private IP range to another private IP range are untouched.
iptables -t nat -A fromprivate -d 192.168.0.0/16 -j ACCEPT
iptables -t nat -A fromprivate -d 172.16.0.0/12 -j ACCEPT
iptables -t nat -A fromprivate -d 10.0.0.0/8 -j ACCEPT
# Packets that get here are from the private address range
# and are trying to get out to the internet. We NAT them.
iptables -t nat -A fromprivate -j MASQUERADE
    
# No-masquerade addresses.
"""
            for item in self.nomasqueradelist:
                stream << "iptables -t nat -A POSTROUTING -d " << item.getAddress() << " -j ACCEPT\n"
    
            stream << """# Siphon off any packets that are from the private IP range.
iptables -t nat -A POSTROUTING -s 192.168.0.0/16 -j fromprivate
iptables -t nat -A POSTROUTING -s 172.16.0.0/12 -j fromprivate
iptables -t nat -A POSTROUTING -s 10.0.0.0/8 -j fromprivate
# packets that get here can just hit the default policy.
"""
        stream << \
"""iptables -t nat -P PREROUTING ACCEPT
iptables -t nat -P POSTROUTING ACCEPT
iptables -t nat -P OUTPUT ACCEPT
iptables -t mangle -P PREROUTING ACCEPT
iptables -t mangle -P POSTROUTING ACCEPT
iptables -t mangle -P OUTPUT ACCEPT
[ $GUIDEDOG_VERBOSE -eq 1 ] && echo """ << '"' << i18n("Finished.") << '"' << """
fi;
true
"""
        return (True,None)
        
    def readScript(self, stream):
        parameterlist = ["# DISABLED=",
            "# ROUTING=",
            "# MASQUERADE=",
            "# MASQUERADEFTP=",
            "# MASQUERADEIRC=",
            "# NOMASQUERADEADDRESS="]
        forwardparameterlist = ["# TYPE=",
            "# ORIGINALPORT=",
            "# SPECIFYORIGINALADDRESS=",
            "# ORIGINALADDRESS=",
            "# NEWPORT=",
            "# SPECIFYNEWADDRESS=",
            "# NEWADDRESS=",
            "# COMMENT="]

        rule = None
    
        stream.setCodec(QTextCodec.codecForName("ISO 8859-1"))
        s = stream.readLine()
        if s.isNull():
            return (False,i18n("Error reading first line of the script."))

        s = stream.readLine()
        if s.isNull():
            return (False,i18n("Error reading second line of the script. Expected [Guidedog]"))
        
        if s!="# [Guidedog]":
            return (False, i18n("Error reading script. This does not appear to be a Guidedog script."))

        # Read past the boring human readable copperplate stuff.
        while True:
            s = stream.readLine();
            if s.isNull():
                return (False,i18n("Error reading script. (Before Description section)."))
            if s=="# [Description]":
                break

        # Read the config description.
        self.description = ""
        addcr = False
        while True:
            s = stream.readLine()
            if s.isNull():
                return (False,i18n("Error reading script. (In Description section)."))
            if s=="# [Config]":
                break
            
            if addcr:
                self.description.append("\n")
            
            addcr = True
            self.description += s[2:]

        # Read the Config
        s = stream.readLine()
        while True:
            if s.isNull():
                return (False,i18n("Error reading script. (In Config section)."))
            if s.startsWith("# [End]") or s.startsWith("# [Forwarding]"):
                break   # We've got to the end of this part of the show.

            # Try to identify the line we are looking at.
            i =0
            for param in parameterlist:
                if s.startsWith(param):
                    rightpart = s[len(param):]
                    break
                i += 1
                
            if i==0:
                self.disabled = rightpart=="1"
            elif i==1:
                self.routing = rightpart=="1"
            elif i==2:
                self.masquerade = rightpart=="1";
            elif i==3:
                self.masqueradeftp = rightpart=="1"
            elif i==4:
                self.masqueradeirc = rightpart=="1"
            elif i==5:
                nomasqaddress = IPRange()
                self.nomasqueradelist.append(nomasqaddress)
                nomasqaddress.setAddress(rightpart)
            else:
                # Should we complain?
                pass
            s = stream.readLine()
        
        while True:
            if s.isNull():
                return (False,i18n("Error reading script. (In forwarding section)."))
        
            if s.startsWith("# [Forwarding]"):
                # Create a new forwarding rule.
                rule = PortForwardRule()
                self.forwardrulelist.append(rule)
            
            if s.startsWith("# [End]"):
                break   # We've got to the end of this part of the show.
    
                # Try to identify the line we are looking at.
            i = 0
            for param in forwardparameterlist:
                if s.startsWith(param):
                    rightpart = s[len(param):]
                    break
                i += 1
                
            if i==0:
                if rightpart=="TCP":
                    rule.iptype = PortForwardRule.IPPROTO_TCP
                else:
                    rule.iptype = PortForwardRule.IPPROTO_UDP
            elif i==1:
                rule.originalport = int(rightpart)
            elif i==2:
                rule.specifyoriginal = rightpart!="0"
            elif i==3:
                rule.originaladdress.setAddress(rightpart)
            elif i==4:
                rule.newport = int(rightpart)
            elif i==5:
                rule.specifynewaddress = rightpart!="0"
            elif i==6:
                rule.newaddress.setAddress(rightpart)
            elif i==7:
                rule.comment = rightpart
            else:
                pass
                
            s = stream.readLine()
            
        return (True,None)

    def openScript(self,filename):
        errorstring = None
        f = QFile(filename)

        if f.open(QIODevice.ReadOnly):
            stream = QTextStream(f)
    
            rc,errorstring = self.readScript(stream)
            f.close()
            return (rc,errorstring)
        else:
            errorstring = i18n("Unable to open the script for reading.")
            return (False,errorstring)

    def saveScript(self,filename):
        errorstring = None
        f = QFile(filename)

        # We want it root executable.
        f.setPermissions(QFile.ReadOwner|QFile.WriteOwner|QFile.ExeOwner)  
        
        rc = f.open(QIODevice.WriteOnly)
        errorstring = None
        
        if rc:
            rc,errorstring = self.writeScript(QTextStream(f))
    
        if not rc:
            if f.error()!=QFile.NoError:
                errorstring = ki18n("An error occurred while writing '%1'. The operating system has this to report about the error: %2") \
                    .subs(filename).subs(f.errorString()).toString()
                f.close()
                return (False,errorstring)
            
            f.close()
            if f.error()!=QFile.NoError:
                errorstring = ki18n("An error occurred while writing '%1'. The operating system has this to report about the error: %2").subs(filename).subs(f.errorString()).toString()
                return (False,errorstring)
            
            return (False,errorstring)

        f.close()
        if f.error()!=QFile.NoError:
            errorstring = ki18n("An error occurred while writing '%1'. The operating system has this to report about the error: %2") \
                .subs(filename).subs(f.errorString()).toString()
            return (False,errorstring)
            
        return (True,None)

if __name__=="__main__":
    import sys
    from PyQt4 import QtGui
    app = QtGui.QApplication(sys.argv)
    guidedogdoc = GuidedogDoc()
    guidedogdoc.openScript("Cpptest_rc.guidedog")
