#! /usr/bin/python
# cnetworkmanager: Command Line Interface for NetworkManager
# by: http://en.opensuse.org/User:Mvidner
# license: http://www.gnu.org/licenses/gpl-2.0.html or later

VERSION = "0.8.4"
print "cnetworkmanager %s - Command Line Interface for NetworkManager" % VERSION

norpm = False
import sys
# find other modules in our prefix, if specified
if len(sys.argv) > 2 and sys.argv[1] == "--prefix":
    prefix = sys.argv[2]
    sys.argv[1:] = sys.argv[3:]
    sys.path.append(prefix + "/share/cnetworkmanager");

import os
import string
import re
import time
import uuid
import math
import hashlib
import pbkdf2
from binascii import hexlify
import ConfigParser             # knm config
from optparse import OptionParser
try:
    import dbus
    import dbus.service
    import _dbus_bindings
except:
    print "Install python-1-dbus.rpm or or python-dbus.rpm or python-dbus.deb"
    norpm = True
import xml.dom.minidom
try:
    import gobject
except:
    # todo - only if loop wanted
    print "Install python-gobject2.rpm or pygobject2.rpm or python-gobject.deb"
    norpm = True
# python-gnome.rpm has gconf for nm-applet...
if norpm:
    sys.exit(1)

from dbus.mainloop.glib import DBusGMainLoop
DBusGMainLoop(set_as_default=True)

LOOP = False

bus = dbus.SystemBus()

# FOOC = connection (service) string
# FOOI = interface string
# fooo = object
# fooi = interface
# foopi = property interface
NMC = 'org.freedesktop.NetworkManager'
NMI = NMC
PI = 'org.freedesktop.DBus.Properties'
SSC = "org.freedesktop.NetworkManagerSystemSettings"
USC = "org.freedesktop.NetworkManagerUserSettings"
NMIC = "org.freedesktop.NetworkManagerInfo"

def introspect(obj):
    ii = dbus.Interface(obj, 'org.freedesktop.DBus.Introspectable')
    print ii.Introspect()

class cNM:
    # TODO: pull them from introspection.xml
    NM_STATE = ["UNKNOWN", "ASLEEP", "CONNECTING", "CONNECTED", "DISCONNECTED",]

    def __init__(self, opath):
	self.opath = opath
        self.nmo = bus.get_object(NMC, self.opath)
        self.nmi = dbus.Interface(self.nmo, NMI)
        self.nmpi = dbus.Interface(self.nmo, PI)

    def Api(self):
        return "common"

    def Dump0(self):
        "Dumps its own info (not owned objects)."
        pass

    def Dump(self):
        self.Dump0()
        if options.dev:
            for device in self.Devices():
                device.Dump()

        if options.actcon:
            print "Active Connections"
            aconns = self.ActiveConnections()
            for aconn in aconns:
                aconn.Dump()

    def ListNets(self):
        print "Wifi Networks:"
        for dev in self.Devices():
            dev.ListNets()

    def err_handler(self, *args):
        print "ERR:", args

    def silent_handler(self, *args):
        pass
        #print "BOO!:", args

    def quitter_handler(self, *args):
        # exit the loop that runs only because of us
        print "padla"
        sys.exit(0)

    def WatchState(self):
        bus.add_signal_receiver(self.state_changed_handler,
                                dbus_interface="org.freedesktop.NetworkManager",
                                signal_name="StateChanged")

    def state_changed_handler(self, *args):
        s = args[0]
        ss = self.NM_STATE[s]
        print time.strftime("(%X)"),
        print "State:", ss


def make_nm(opath):
    "Detects NM version and chooses appropriate class"

    nmo = bus.get_object(NMC, opath)
    nmi = dbus.Interface(nmo, NMI)
    try:
        dummy = nmi.getDevices()
        return cNM_06(opath)
    except dbus.exceptions.DBusException:
        return cNM_07(opath)

class cNM_06(cNM):
    def Api(self):
        return "06"

    def SetWifiEnabled(self, v):
        # TODO: async call, catch the state signal and exit
        # weird: synchronous call works, but times out
        # asynchronous call does not work
        self.nmi.setWirelessEnabled(v,
                                    reply_handler=self.quitter_handler,
                                    error_handler=self.quitter_handler)
        global LOOP
        LOOP = True

    def SetOnline(self, v):
        if v:
            self.nmi.wake(True,
                                    reply_handler=self.quitter_handler,
                                    error_handler=self.quitter_handler)
        else:
            self.nmi.sleep(True,
                                    reply_handler=self.quitter_handler,
                                    error_handler=self.quitter_handler)
        global LOOP
        LOOP = True

    def Dump0(self):
        print "State:", self.NM_STATE[self.nmi.state()]
        we = self.nmi.getWirelessEnabled()
        if isinstance(we, tuple):
            print "Wifi enabled:", bool(we[0])
            print "Wifi HW enabled:", bool(we[1])
        else:
            print "Wifi enabled:", bool(we)
        
        try:
            dup = self.nmi.getDialup()
            print "Dialup:", dup
        except dbus.exceptions.DBusException, e:
            #if e.get_dbus_name() == "org.freedesktop.NetworkManager.NoDialup":
            #    pass
            #else:
            print e

    def Devices(self):
        opaths = self.nmi.getDevices()
        return map(cDevice_06, opaths)

    def ActiveConnections(self):
        return []               # at most one active connection, FIXME find it

    def ActivateConnection(self, conn, device, ap):
        # passing *_handler makes the call asynchronous
        self.nmi.setActiveDevice(device.opath, ssid_str(conn.Ssid()),
                                    reply_handler=self.silent_handler,
                                    error_handler=self.err_handler,
                                    )

class cNM_07(cNM):
    def Api(self):
        return "07"

    def SetWifiEnabled(self, v):
        self.nmpi.Set(NMI, "WirelessEnabled", v)

    def SetOnline(self, v):
        self.nmi.Sleep(not v)

    def Dump0(self):
        print "State:", self.NM_STATE[self.nmpi.Get(NMI, "State")]
        print "Wifi enabled:", self.nmpi.Get(NMI, "WirelessEnabled")
        print "Wifi HW enabled:", self.nmpi.Get(NMI, "WirelessHardwareEnabled")

    def Devices(self):
        opaths = self.nmi.GetDevices()
        return map(cDevice_07, opaths)

    def ActiveConnections(self):
        aconns = self.nmpi.Get(NMI, "ActiveConnections")
        return map(cActiveConnection, aconns)

    def ActivateConnection(self, conn, device, ap):
        # passing *_handler makes the call asynchronous
        self.nmi.ActivateConnection(USC,
                                    conn.__dbus_object_path__,
                                    device.opath,
                                    ap.opath,
                                    reply_handler=self.silent_handler,
                                    error_handler=self.err_handler,
                                    )


class cActiveConnection:
    def __init__(self, opath):
        self.opath = opath

    def Dump(self):
       print self.opath
       co = bus.get_object(NMC, self.opath)
       copi = dbus.Interface(co, PI)
       for P in ["ServiceName", "Connection", "SpecificObject",]:
           print "  %s: %s" % (P, copi.Get(NMI, P))
       devs = copi.Get(NMI, "Devices")
       print "  Devices:"
       for dev in devs:
           print "  ", dev
   
def bitmask_str(map, value):
    ret = []
    for mask in sorted(map.keys()):
        if value & mask: ret.append(map[mask])
    return ",".join(ret)


class cDevice:
    def __init__(self, opath):
        self.opath = opath
        self.devo = bus.get_object(NMC, self.opath)
        self.devi = dbus.Interface(self.devo, NMI + ".Device")
        self.devpi = dbus.Interface(self.devo, PI)
        self.dt = None
        self.DeviceType0()

    DEVICE_TYPE = ["UNKNOWN", "802_3_ETHERNET", "802_11_WIRELESS",
                   "GSM", "CDMA",] #OLPC: 3 is MESH

    def DeviceType(self):
        return self.DEVICE_TYPE[self.DeviceType0()]

    def ip_str(self, i32):
        ret = []
        ret.append("%d" % (i32 % 256))
        i32 /= 256
        ret.append("%d" % (i32 % 256))
        i32 /= 256
        ret.append("%d" % (i32 % 256))
        i32 /= 256
        ret.append("%d" % (i32 % 256))
        i32 /= 256
        return ".".join(ret)

    def DumpIp4Config(self, opath):
        print "   Ip4Config:", opath
        o = bus.get_object(NMC, opath)
        pi = dbus.Interface(o, PI)
        try:
            for P in ["Address", "Netmask", "Broadcast", "Gateway",]: # beta2?
                print "    %s: %s" % (P, self.ip_str(pi.Get(NMI, P)))
        except:
            print "    Addresses:"
            addrs = pi.Get(NMI, "Addresses")
            for addr in addrs:
                print "     %s/%s via %s" %  tuple(map(self.ip_str, addr))
        nss = pi.Get(NMI, "Nameservers")
        print "    Nameservers:", " ".join(map(self.ip_str, nss))
        doms = pi.Get(NMI, "Domains")
        print "    Domains:", " ".join(doms)

    NM_DEVICE_CAP = {1: "NM_SUPPORTED", 2: "CARRIER_DETECT", 4: "SCANNING", }

    
    def Dump(self):
        print "Device:", self.opath


    IW_MODE = ["AUTO", "ADHOC", "INFRA", "MASTER",
               "REPEAT", "SECOND", "MONITOR",]

    def APs(self):
        return []

    def ListNets(self):
            for ap in self.APs():
                ap.ListNets()

# mixin
class cDeviceEth:
    pass

class cDevice_06(cDevice):
    def DeviceType0(self):
        if self.dt is None:
            self.dt = self.devi.getProperties()[2]
            if self.dt == 1:
                self.__class__ = cDeviceEth_06
            elif self.dt == 2:
                self.__class__ = cDeviceWifi_06
        return self.dt

    NM_ACT_STAGE = [
        "UNKNOWN", "DEVICE_PREPARE", "DEVICE_CONFIG", "NEED_USER_KEY",
        "IP_CONFIG_START", "IP_CONFIG_GET", "IP_CONFIG_COMMIT",
        "ACTIVATED", "FAILED", "CANCELLED", ]

    def Dump(self):
        cDevice.Dump(self)
        print "  Driver:", self.devi.getDriver()
        props = self.devi.getProperties() # osusb ussss sssii biuus as
        print "  Self:", props[0]         # o
        print "  Interface:", props[1]    # s
        print "  Type:", self.DEVICE_TYPE[props[2]] # u
        print "  UDI:", props[3]                    # s
        print "  Active:", bool(props[4])           # b
        print "  Activation Stage:", self.NM_ACT_STAGE[props[5]] # u
        print "  IP:", props[6]                     # s
        print "  Mask:", props[7]                   # s
        print "  Bcast:", props[8]                  # s
        print "  HwAddress:", props[9]              # s
        print "  GW:", props[10]                    # s
        print "  NS1:", props[11]                   # s
        print "  NS2:", props[12]                   # s
        self.DumpMore()

    def DumpMore(self):
	print "  (unknown device type, not dumping more)"

class cDeviceEth_06(cDevice_06, cDeviceEth):
    def DumpMore(self):
        props = self.devi.getProperties() # osusb ussss sssii biuus as
        print "  Link Active:", bool(props[15])     # b
        print "  Speed:", props[16]                 # i
        print "  Generic Capabilities:", bitmask_str(self.NM_DEVICE_CAP, props[17])  # u

class cDeviceWifi_06(cDevice_06):
    NM_802_11_CAP = {
	0x00000001: "PROTO_NONE",
	0x00000002: "PROTO_WEP",
	0x00000004: "PROTO_WPA",
	0x00000008: "PROTO_WPA2",
	0x00000010: "RESERVED1",
	0x00000020: "RESERVED2",
	0x00000040: "KEY_MGMT_PSK",
	0x00000080: "KEY_MGMT_802_1X",
	0x00000100: "RESERVED3",
	0x00000200: "RESERVED4",
	0x00000400: "RESERVED5",
	0x00000800: "RESERVED6",
	0x00001000: "CIPHER_WEP40",
	0x00002000: "CIPHER_WEP104",
	0x00004000: "CIPHER_TKIP",
	0x00008000: "CIPHER_CCMP",
        }

    def APs(self):
        self.wdevi = dbus.Interface(self.devo, NMI + ".Device.Wireless")
        aps = self.devi.getProperties()[20]
        return map(cAP_06, aps)

    def DumpMore(self):
        props = self.devi.getProperties() # osusb ussss sssii biuus as
        print "  Mode:", self.IW_MODE[props[13]]    # i
        print "  Strength:", props[14]              # i
        print "  Link Active:", bool(props[15])     # b
        print "  Speed:", props[16]                 # i
        print "  Generic Capabilities:", bitmask_str(self.NM_DEVICE_CAP, props[17])  # u
        print "  Capabilities:", bitmask_str(self.NM_802_11_CAP, props[18]) # u
        print "  Current net:", props[19]           # s
        nets = props[20]                            # as
        print "  Seen nets:", " ".join(nets)
        if options.ap:
            print "  Access Points"
            for ap in self.APs():
                ap.Dump()

class cDevice_07(cDevice):
    def DeviceType0(self):
        if self.dt is None:
            self.dt = self.devpi.Get(NMI, "DeviceType")
            if self.dt == 1:
                self.__class__ = cDeviceEth_07
            elif self.dt == 2:
                self.__class__ = cDeviceWifi_07
        return self.dt

    NM_DEVICE_STATE = [
        "UNKNOWN", "UNMANAGED", "UNAVAILABLE", "DISCONNECTED", "PREPARE",
        "CONFIG", "NEED_AUTH", "IP_CONFIG", "ACTIVATED", "FAILED",]

    def Dump(self):
        cDevice.Dump(self)

        # "Ip4Config", only for NM_DEVICE_STATE_ACTIVATED
        for P in ["Udi", "Interface", "Driver",]:
            print "  %s: %s" % (P, self.devpi.Get(NMI, P))
        addr = self.devpi.Get(NMI, "Ip4Address")
        print "  Ip4Address:", self.ip_str(addr)
        caps = self.devpi.Get(NMI, "Capabilities")
        print "  Capabilities:", bitmask_str(self.NM_DEVICE_CAP, caps)
        state = self.NM_DEVICE_STATE[self.devpi.Get(NMI, "State")]
        print "  Dev State:", state
        if state == "ACTIVATED":
            self.DumpIp4Config(self.devpi.Get(NMI, "Ip4Config"))

        dt = self.DeviceType()
        print "  Dev Type:", dt
        self.DumpMore()

class cDeviceEth_07(cDevice_07, cDeviceEth):
    def DumpMore(self):
        for P in ["HwAddress", "Speed", "Carrier"]:
            print "  %s: %s" % (P, self.devpi.Get(NMI, P))

class cDeviceWifi_07(cDevice_07):
    NM_802_11_DEVICE_CAP = {1:"CIPHER_WEP40", 2:"CIPHER_WEP104",
                            4:"CIPHER_TKIP", 8:"CIPHER_CCMP",
                            16:"WPA", 32:"RSN",}

    def APs(self):
        self.wdevi = dbus.Interface(self.devo, NMI + ".Device.Wireless")
        aps = self.wdevi.GetAccessPoints()
        return map(cAP_07, aps)

    def DumpMore(self):
        print "  Dev Mode:", self.IW_MODE[self.devpi.Get(NMI, "Mode")]
        wcaps = self.devpi.Get(NMI, "WirelessCapabilities")
        print "  Wifi Capabilities:", bitmask_str(self.NM_802_11_DEVICE_CAP, wcaps)
        for P in ["HwAddress", "Bitrate", "ActiveAccessPoint"]:
            print "  %s: %s" % (P, self.devpi.Get(NMI, P))
        if options.ap:
            print "  Access Points"
            for ap in self.APs():
                ap.Dump()
    
"""An AP found around us"""
class cAP:
    def __init__(self, opath):
        self.opath = opath
        self.apo = bus.get_object(NMC, self.opath)
        self.appi = dbus.Interface(self.apo, PI)
        # for _06
        self.devi = dbus.Interface(self.apo, NMI + ".Devices")

    NM_802_11_AP_FLAGS = {1: "PRIVACY",}

    NM_802_11_AP_SEC = {
        1: "PAIR_WEP40", 2: "PAIR_WEP104", 4: "PAIR_TKIP", 8: "PAIR_CCMP",
        16: "GROUP_WEP40", 32: "GROUP_WEP104", 64: "GROUP_TKIP",
        128: "GROUP_CCMP", 256: "KEY_MGMT_PSK", 512: "KEY_MGMT_802_1X",}

    def ListNets(self, marker = " "):
        # TODO *mark current
        mbr = self.Mbr() / 1024 # 07 1000, 06 1024?
        priv_s = self.PrivS()
        print "%s%3d: %s (%dMb%s)" % (marker, self.Strength(), self.Ssid(), mbr, priv_s)

class cAP_06(cAP):
    def Mbr(self, props=None):
        if props is None:
            props = self.devi.getProperties()
        return props[5]

    
    def PrivS(self):
        props = self.devi.getProperties()
        caps_s = bitmask_str(cDeviceWifi_06.NM_802_11_CAP, props[7]) + ","
        priv_s = ""
        if caps_s.find("PROTO_WEP,") != -1:
            priv_s += " WEP"
        if caps_s.find("PROTO_WPA,") != -1:
            priv_s += " WPA"
        if caps_s.find("PROTO_WPA2,") != -1:
            priv_s += " WPA2"
        if caps_s.find("KEY_MGMT_802_1X,") != -1:
            priv_s += " Enterprise"
        return priv_s

    def Strength(self, props=None):
        if props is None:
            props = self.devi.getProperties()
        return props[3]

    def Ssid(self, props=None):
        if props is None:
            props = self.devi.getProperties()
        return props[1]


    def Dump(self):
        props = self.devi.getProperties() # ossid iiib
        print "   Self:", props[0]
        print "    Ssid:", self.Ssid(props)
        print "    HwAddress:", props[2]
        print "    Strength:", self.Strength(props)
        print "    Frequency:", props[4]
        print "    MaxBitrate:", self.Mbr(props)
        print "    AP Mode:", cDevice.IW_MODE[props[6]]
        print "    Capabilities:", bitmask_str(cDeviceWifi_06.NM_802_11_CAP, props[7])
        print "    Broadcast:", props[8]
        
def ssid_str(array):
    s = ""
    for b in array:
        s = s + ("%c" % b)
    return s

def opath_validchar(c):
    # _ is also escaped even though it is valid
    return \
        string.ascii_letters.find(c) != -1 or \
        string.digits.find(c) != -1

def opath_escape(s):
    r = ""
    for c in s:
        # TODO find a more elegant way
        if not opath_validchar(c):
            # "-" -> "_2d_"
            c = "_%2x_" % ord(c)
        r = r + c
    return r

def opath_unescape(s):
    # "2d" -> "-"
    unhex = lambda xx: chr(eval("0x"+xx))
    # all "_2d_" -> "-"
    return re.sub("_.._", lambda p: unhex(p.group()[1:3]), s)

class cAP_07(cAP):
    def Mbr(self):
        return self.appi.Get(NMI, "MaxBitrate")

    def PrivS(self):
        priv = self.appi.Get(NMI, "Flags") != 0
        wpa = self.appi.Get(NMI, "WpaFlags") != 0
        wpa2 = self.appi.Get(NMI, "RsnFlags") != 0
        priv_s = ""
        if priv:
            if not wpa and not wpa2:
                priv_s = priv_s + " WEP"
            if wpa:
                priv_s = priv_s + " WPA"
            if wpa2:
                priv_s = priv_s + " WPA2"
        return priv_s

    def Strength(self):
        return int(self.appi.Get(NMI, "Strength"))

    def Ssid(self):
        return ssid_str(self.appi.Get(NMI, "Ssid"))

    def Dump(self):
        print "  AP:", self.opath
        print "    Ssid:", self.Ssid()
        for P in ["Frequency", "HwAddress", "MaxBitrate",]:
            print "    %s: %s" % (P, self.appi.Get(NMI, P))
        print "    Strength:", self.Strength()
        print "    AP Mode:", cDevice.IW_MODE[self.appi.Get(NMI, "Mode")]
        print "    AP Flags:", bitmask_str(self.NM_802_11_AP_FLAGS,
                                           self.appi.Get(NMI, "Flags"))
        print "    AP WPA Flags:", bitmask_str(self.NM_802_11_AP_SEC,
                                               self.appi.Get(NMI, "WpaFlags"))
        print "    AP RSN Flags:", bitmask_str(self.NM_802_11_AP_SEC,
                                               self.appi.Get(NMI, "RsnFlags"))
    
# this is the client side of the applet; see also UserSettings
class cApplet:
    def __init__(self, svc, opath):
        self.svc = svc
        self.opath = opath
        self.so = bus.get_object(self.svc, self.opath)
        self.si = dbus.Interface(self.so, 'org.freedesktop.NetworkManagerSettings')

    def isSystem(self):
        return self.svc == SSC;

    def Dump(self):
        for conn in self.Connections():
            conn.Dump()
        if self.isSystem():
            self.DumpSystem()

    def DumpSystem(self):
        sspi = dbus.Interface(self.so, PI)
        print "Unmanaged Devices"
        umds = sspi.Get(NMI, "UnmanagedDevices")
        for umd in umds:
            print " ", umd
           #  dump_settings_conn(svc, conn) umd?


    def myConnection(self, opath):
        return cConnection(self.svc, opath)

    def Connections(self):
        opaths = self.si.ListConnections()
        return map(self.myConnection, opaths)

NETWORK_TYPE_ALLOWED = 1
class cApplet_06(cApplet):
    def __init__(self, svc, opath):
        self.svc = svc
        self.opath = opath
        self.io = bus.get_object(self.svc, self.opath)
        self.ii = dbus.Interface(self.io, 'org.freedesktop.NetworkManagerInfo')

    def isSystem(self):
        return False;

    def myConnection(self, opath):
        return cConnection_06(self, opath)

    # TODO also VPN conns
    def Connections(self):
        names = self.ii.getNetworks(NETWORK_TYPE_ALLOWED)
        return map(self.myConnection, names)

class cConnection:
    def __init__(self, svc, opath):
        self.svc = svc
        self.opath = opath
        self.co = bus.get_object(self.svc, self.opath)
        self.ci = dbus.Interface(self.co, 'org.freedesktop.NetworkManagerSettings.Connection')

    def Dump(self):
       print "Conn:", self.opath
       settings = self.Settings()
       settings.Dump()

       si = dbus.Interface(self.co, 'org.freedesktop.NetworkManagerSettings.Connection.Secrets')
       security = settings.Security()
       if security != "":
           print " SECRETS:", security
           try:
               # TODO merge them
               secrets = cSettings(si.GetSecrets(security,[],False))
               secrets.Dump()
           except dbus.exceptions.DBusException, e:
               if e.get_dbus_name() == "org.freedesktop.DBus.Error.AccessDenied":
                   print "   Access denied"
               else:
                   print "  ", e
                   print "   FIXME figure out 802-1x secrets"

    def Settings(self):
        return cSettings(self.ci.GetSettings())

def dump_time(unixtime):
    return time.asctime(time.localtime(unixtime))

class cConnection_06:
    def __init__(self, applet, id):
        self.id = id
        self.applet = applet

    def Dump(self):
        print "Conn:", self.id

        np = self.applet.ii.getNetworkProperties(self.id, NETWORK_TYPE_ALLOWED)
        ssid = np[0]
        print " ssid:", ssid
        print " time:", dump_time(np[1])
        print " trusted:", bool(np[2])
        print " bssids:", ", ".join(np[3])
        enctype = np[4]
        print " we_cipher:", enctype
        if enctype != 1:
            print " secret:", np[5]
        if enctype == 16:
            print " wep_auth_algorithm:", np[6]
        elif enctype == 0:
            print " wpa_psk_key_mgt:", np[6]
            print " wpa_psk_wpa_version:", np[7]

        return # nm-applet will not tell kfn anyway
        devp = "/org/freedesktop/NetworkManager/Devices/ath0" #FIXME
        netp = devp + "/Networks/" + opath_escape(self.id)
        attempt = 1
        newkey = False
        kfn = self.applet.ii.getKeyForNetwork(devp, netp, ssid, attempt, newkey)
        print " kfn:", kfn


# 06
NM_AUTH_TYPE_WPA_PSK_AUTO = 0x00000000
NM_AUTH_TYPE_NONE         = 0x00000001
NM_AUTH_TYPE_WEP40        = 0x00000002
NM_AUTH_TYPE_WPA_PSK_TKIP = 0x00000004
NM_AUTH_TYPE_WPA_PSK_CCMP = 0x00000008
NM_AUTH_TYPE_WEP104       = 0x00000010
NM_AUTH_TYPE_WPA_EAP      = 0x00000020
NM_AUTH_TYPE_LEAP         = 0x00000040

IW_AUTH_ALG_OPEN_SYSTEM   = 0x00000001
IW_AUTH_ALG_SHARED_KEY    = 0x00000002
IW_AUTH_ALG_LEAP          = 0x00000004

class cSettings:
    def __init__(self, conmap):
        #print "INIT", conmap
        self.conmap = conmap

    def Type(self):
        return self.conmap["connection"]["type"]

    def ID(self):
        return self.conmap["connection"]["id"]

    def Ssid(self):
        try:
            return self.conmap["802-11-wireless"]["ssid"]
        except KeyError:
            pass
        # probably 802-3-ethernet
        return ""

    def Timestamp(self):
        try:
            return self.conmap["connection"]["timestamp"]
        except KeyError:
            return 0

    def Trusted(self):
        # false by default
        return False

    def SeenBssids(self):
        try:
            return self.conmap["802-11-wireless"]["seen-bssids"]
        except KeyError:
            return []

    # for 06
    def WeCipher(self):
        k = self.Key()
        if len(k) == 26:
            return NM_AUTH_TYPE_WEP104
        elif len(k) == 64:
            return NM_AUTH_TYPE_WPA_PSK_AUTO
        elif len(k) == 0:
            return NM_AUTH_TYPE_NONE
        print "Defaulting cipher type to none"
        return NM_AUTH_TYPE_NONE

    def Key(self):
        try:
            return self.conmap["802-11-wireless-security"]["psk"]
        except KeyError:
            pass
        try:
            return self.conmap["802-11-wireless-security"]["wep-key0"]
        except KeyError:
            pass
        # no key
        return ""

    def WepAuthAlgorithm(self):
        print "FIXME Defaulting WEP auth alg to open"
        return IW_AUTH_ALG_OPEN_SYSTEM

    def PskKeyMgt(self):
        print "FIXME Defaulting PSK key mgmt to 2"
        return 2

    def PskWpaVersion(self):
        print "FIXME Defaulting WPA version to 2"
        return 2

    def Security(self):
        try:
            return self.conmap[self.Type()]["security"]
        except KeyError:
            return ""

    def isNet(self, net_name):
        return self.ID() == net_name or self.Ssid() == net_name

    # FIXME check spec/NM what to censor
    secrets = dict.fromkeys(["wep-key0", "psk"])

    def ConMap(self):
        "For GetSettings: censor secrets."

        cm = dict()
        for n1, v1 in self.conmap.iteritems():
            cm[n1] = dict()
            for n2, v2 in v1.iteritems():
                cv2 = v2
                if self.secrets.has_key(n2):
                    cv2 = ""
                cm[n1][n2] = cv2
        return cm

    def SecMap(self):
        "For GetSecrets: only secrets."
        s = self.Security()
        r = {
            s: self.conmap[s]
            }
        print "SECMAP", r
        return r

    def Dump(self):
        for n1, v1 in self.conmap.iteritems():
            print " ",n1
            for n2, v2 in v1.iteritems():        
                print "   %s: %s" % (n2, v2)

def mkconmap_wifi(ssid):
    return {
        'connection': {
            'id': '_cnm_handcrafted_',
            'uuid': str(uuid.uuid1()), # new in oS 11.1
            'type': '802-11-wireless',
            },
        '802-11-wireless': {
            'ssid': dbus.ByteArray(ssid),
            'mode': 'infrastructure',
            },
        }

def elongate(s, tlen):
    "repeat string s to target length tlen"
    if s == "":
        return ""
    copies_needed = int(math.ceil(tlen / float(len(s))))
    return (s * copies_needed)[:tlen]

# http://www.mail-archive.com/networkmanager-list@gnome.org/msg07935.html
def wep_passphrase_to_hash(p):
    return hashlib.md5(elongate(p, 64)).hexdigest()

def mkconmap_wep_pass(ssid, key):
    cm = mkconmap_wifi(ssid)
    cm["802-11-wireless"]["security"] = "802-11-wireless-security"
    cm["802-11-wireless-security"] = {}
    cm["802-11-wireless-security"]["key-mgmt"] = "none"
    cm["802-11-wireless-security"]["wep-tx-keyidx"] = 0
    cm["802-11-wireless-security"]["wep-key0"] = wep_passphrase_to_hash(key)
    return cm

def mkconmap_wep(ssid, key):
    cm = mkconmap_wifi(ssid)
    cm["802-11-wireless"]["security"] = "802-11-wireless-security"
    cm["802-11-wireless-security"] = {}
    cm["802-11-wireless-security"]["key-mgmt"] = "none"
    cm["802-11-wireless-security"]["wep-tx-keyidx"] = 0
    cm["802-11-wireless-security"]["wep-key0"] = key
    return cm

def mkconmap_psk(ssid, key):
    cm = mkconmap_wifi(ssid)
    cm["802-11-wireless"]["security"] = "802-11-wireless-security"
    cm["802-11-wireless-security"] = {}
    cm["802-11-wireless-security"]["key-mgmt"] = "wpa-psk"
    cm["802-11-wireless-security"]["psk"] = key
    cm["802-11-wireless-security"]["group"] =    ["tkip", "ccmp"]
    cm["802-11-wireless-security"]["pairwise"] = ["tkip", "ccmp"]
    return cm


# server analog of cApplet
class UserSettings(dbus.service.Object):
    # conmaps is a list
    def __init__(self, opath, conmaps):
        dbus.service.Object.__init__(self, bus, opath)
        #print "CONMAPS:", conmaps
        self.conns = map(self.newCon, conmaps)

    def addCon(self, conmap):
        c = self.newCon(conmap)
        self.conns.append(c)
        return c

    counter = 1
    def newCon(self, conmap):
        cpath = "/MyConnection/%d" % self.counter
        self.counter = self.counter + 1
        c = Connection(cpath, conmap)
        self.NewConnection(cpath) # announce it
        return c

    @dbus.service.method(dbus_interface='org.freedesktop.NetworkManagerSettings',
                             in_signature='', out_signature='ao')
    def ListConnections(self):
        return [c.__dbus_object_path__ for c in self.conns]

    #this is for EMITTING a signal, not receiving it
    @dbus.service.signal(dbus_interface='org.freedesktop.NetworkManagerSettings',
                             signature='o')
    def NewConnection(self, opath):
        pass
        #print "signalling newconn:", opath

    def GetByNet(self, net_name):
        "Returns connection, or None"
        for c in self.conns:
            if c.isNet(net_name):
                return c
        return None


class UserSettings_06(UserSettings):
    # conmaps is a list
    def __init__(self, opath, conmaps):
        dbus.service.Object.__init__(self, bus, opath)
        #print "CONMAPS:", conmaps
        self.conns = map(self.newCon, conmaps)

    counter = 1
    def newCon(self, conmap):
        cpath = "/MyConnection/%d" % self.counter
        self.counter = self.counter + 1
        c = Connection_06(cpath, conmap)
        #self.NewConnection(cpath) # announce it
        return c

    @dbus.service.method(dbus_interface="org.freedesktop.NetworkManagerInfo",
                         in_signature="i", out_signature='as')
    def getNetworks(self, i):
        # FIXME bytearray to str WHERE?
        #n = [ssid_str(c.Ssid()) for c in self.conns]
        n = [c.ID() for c in self.conns]
        print "getNetworks:", n
        return n

    @dbus.service.method(dbus_interface="org.freedesktop.NetworkManagerInfo",
                         in_signature="", out_signature='ao') # out??
    def getVPNConnections(self):
        return []               # FIXME

    @dbus.service.method(dbus_interface="org.freedesktop.NetworkManagerInfo",
                         in_signature="si")
                         #out_signature='sibasi') #varies
    def getNetworkProperties(self, net, type):
        print "GNP", net
        # type is 1, NETWORK_TYPE_ALLOWED
        c = self.GetByNet(net)
        if c != None:
            return c.getNetworkProperties()
        print "Oops, could not getNetworkProperties for " + net
        

    @dbus.service.method(dbus_interface="org.freedesktop.NetworkManagerInfo",
                         in_signature="oosib")
                         #out_signature="isi") varies
    def getKeyForNetwork(self, dev, net, ssid, attempt, newkey):
        print "GKFN", dev, net, ssid, attempt, bool(newkey)
        if newkey:
            m = "Cannot ask for key"
            print m
            raise dbus.exceptions.DBusException(m)

        snet = opath_unescape(net[net.rfind("/")+1 : ]) # only stuff after /
        c = self.GetByNet(snet)
        if c != None:
            return c.getKeyForNetwork()
        print "Oops, could not getKeyForNetwork " + net

    @dbus.service.method(dbus_interface="org.freedesktop.NetworkManagerInfo",
                         out_signature='')
                         #in_signature="sbs isi", varies
    def updateNetworkInfo(self, ssid, automatic, bssid, *security):
        print "Connected successfully"
        return
        print "UNI"
        print " ssid:", ssid
        print " automatic:", bool(automatic)
        print " bssid:", bssid
        print " security:", security


    def GetByNet(self, net_name):
        "Returns connection, or None"
        for c in self.conns:
            if c.isNet(net_name):
                return c
        return None


# server analog of cConnection
class Connection(dbus.service.Object):
    def __init__(self, opath, conmap):
        dbus.service.Object.__init__(self, bus, opath)
        self.settings = cSettings(conmap)

    @dbus.service.method(dbus_interface='org.freedesktop.NetworkManagerSettings.Connection',
                         sender_keyword='sender',
                             in_signature='', out_signature='a{sa{sv}}')
    def GetSettings(self, sender):
        #print "Getting settings:", self. __dbus_object_path__
#        return self.settings.ConMap()
# grr, censoring secrets makes NM complain!?
        # bnc#479566#c3: Until I figure out how to make it work with
        # censored secrets, only pass the settings to the same user.
        sender_uid = bus.get_unix_user(sender)
        if sender_uid != 0 and sender_uid != os.geteuid():
            e = "User %u is not permitted to read the settings" % sender_uid
            print e
            raise dbus.exceptions.DBusException(e) # could do NM_SETTINGS_ERROR_* instead
        return self.settings.conmap

    @dbus.service.method(dbus_interface='org.freedesktop.NetworkManagerSettings.Connection.Secrets',
                             in_signature='sasb', out_signature='a{sa{sv}}')
    def GetSecrets(self, tag, hints, ask):
        # FIXME respect args
        print "Getting secrets:", self.__dbus_object_path__
        return self.settings.SecMap()

    @dbus.service.method(dbus_interface='org.freedesktop.NetworkManagerSettings.Connection',
                             in_signature='', out_signature='s')
    def ID(self):
        return self.settings.ID()

    def Ssid(self):
        return self.settings.Ssid()

    def isNet(self, net_name):
        return self.settings.isNet(net_name)

class Connection_06(Connection):
    def __init__(self, opath, conmap):
        dbus.service.Object.__init__(self, bus, opath)
        #print "C6", conmap
        self.settings = cSettings(conmap)

    # dbus.service.method
    def getNetworkProperties(self):
        # essid, timestamp, ?, bssids, we_cipher, ?, ...
        # we_cipher=16: i wep_auth_algorithm
        # we_cipher=0:  i wpa_psk_key_mgt, i wpa_psk_wpa_version
        ssid = ssid_str(self.settings.Ssid())
        time = self.settings.Timestamp() # last sucessfully connected? seen?
        trusted = self.settings.Trusted()
        bssids = dbus.Array(self.settings.SeenBssids(), signature="s")
        r = [ssid, time, trusted, bssids]
        security = self.getKeyForNetwork("fake key")
        r.extend(security)
        return tuple(r)

    # dbus.service.method
    def getKeyForNetwork(self, fake="no"):
        if fake == "no":
            key = self.settings.Key()
        else:
            key = ""

        # security
        cip = self.settings.WeCipher()
        if cip == NM_AUTH_TYPE_NONE:
            security = tuple([cip])
        elif cip == NM_AUTH_TYPE_WEP40 or cip == NM_AUTH_TYPE_WEP104:
            wep_auth_algorithm = self.settings.WepAuthAlgorithm()
            security = (cip, key, wep_auth_algorithm)
        elif cip == NM_AUTH_TYPE_WPA_PSK_AUTO or cip == NM_AUTH_TYPE_TKIP or \
                cip == NM_AUTH_TYPE_CCMP:
            wpa_psk_key_mgt = self.settings.PskKeyMgt()
            wpa_psk_wpa_version = self.settings.PskWpaVersion()
            security = (cip, key, wpa_psk_key_mgt, wpa_psk_wpa_version)
        elif cip == NM_AUTH_TYPE_WPA_EAP:
            security = tuple([cip]) # TODO more...
        elif cip == NM_AUTH_TYPE_LEAP:
            security = tuple([cip]) # TODO more...
        return security


class ConfigParserKNM:
    "Parse ~/.kde/share/config/knetworkmanagerrc"

    def __init__(self):
        p = ConfigParser.RawConfigParser()
        ok = p.read(os.getenv("HOME") + "/.kde/share/config/knetworkmanagerrc")

        self.conmaps_d = {}
        for s in p.sections():
            path = s.split("_")
            #print "##", path
            if path[0] in ["ConnectionSetting", "ConnectionSecrets"]:
                cid = path[1]
                self.conmaps_d.setdefault(cid, {})
                part = path[2]

                values = {}
                for (n, v) in p.items(s):
                    # WTF, Value_ is transformed to value_
                    if n[:6]  == "value_":
                        n = n[6:]
                        v = self.ParseValue(v)
                        #print "# %s:%s" % (n, v)
                        # do not overwrite ConnectionSecrets
                        # with empty ConnectionSettings field
                        try:
                            vv = self.conmaps_d[cid][part][n]
                        except KeyError:
                            vv = ""
                        if vv == "":
                            values[n] = v
                if len(values) != 0: # empty 802-1x confuses NM!?
                    self.conmaps_d[cid].setdefault(part, {})
                    self.conmaps_d[cid][part].update(**values)
                #print "PARSED", cid, part, values

    def ConMaps(self):
        return self.conmaps_d.values()

    def ParseValue(self, v):
        v = eval('"%s"' % v)    # unescape backslashes
        dom = xml.dom.minidom.parseString(v)
        return self.ParseNode(dom.documentElement)

    def ParseNode(self, n):
        t = n.localName
        if t != "list":
            v = self.NodeText(n)

        if t == "string":
            return v
        elif t == "byte":
            return dbus.Byte(int(v))
        elif t == "bool":
            return v == "true"
        elif t == "int32" or t == "uint32":
            return int(v)
        elif t == "list":
            v = []
            c = n.firstChild
            while c != None:
                if c.localName != None: # whitespace
                    v.append(self.ParseNode(c))
                c = c.nextSibling
            return v

    def NodeText(self, n):
        if n.hasChildNodes():
            return n.firstChild.wholeText
        else:
            return ""

class MonitorBase:
    def __init__(self):
        self.amap = {}
        self.specific = {}
        bus.add_signal_receiver(self.abbr_h,
                        path_keyword="path",
                        interface_keyword="interface",
                        member_keyword="member")

    def ignore(self, dbus_interface, signal_name):
        self.watch(self.null_h, dbus_interface, signal_name)

    def null_h(self, *args, **kwargs):
        pass

    def watch(self, handler, dbus_interface, signal_name):
        self.specific[dbus_interface +"."+ signal_name] = True
        bus.add_signal_receiver(handler,
                                dbus_interface=dbus_interface,
                                signal_name=signal_name,
                                path_keyword="path")

    def abbr_h(self, *args, **kwargs):
        ifc = kwargs["interface"]
        sig = kwargs["member"]
        if self.specific.has_key(ifc +"."+ sig):
            return

        opath = kwargs["path"]
        line = "SIG %s: %s.%s%s" % (self.abbrev(opath,"/"),
                                    self.abbrev(ifc,"."),
                                    sig, args)
        print line

    def abbrev(self, s, sep):
        words = s.split(sep)
        words = map (self.a1, words)
        result = sep.join(words)
        if self.amap.has_key(result):
            if self.amap[result] != s:
                print "ABBR COLLISION %s was %s now %s" % (result, self.amap[result], s)
        else:
            print "ABBR %s is %s" % (result, s)
        self.amap[result] = s
        return result
    
    def a1(self, s):
        if s == "":
            return ""
        #print "#A", s
        # first char, delete lowercase and _ from the rest
        return s[0] + s[1:].translate(string.maketrans("", ""),
                                      string.lowercase + "_")

class Monitor(MonitorBase):
    def __init__(self):
        MonitorBase.__init__(self)

        self.watch(
            self.propc_h,
            dbus_interface="org.freedesktop.NetworkManager.Device.Wireless",
            signal_name="PropertiesChanged")
        self.watch(
            self.propc_h,
            dbus_interface="org.freedesktop.NetworkManager.AccessPoint",
            signal_name="PropertiesChanged")

        self.ignore("org.freedesktop.Hal.Device", "PropertyModified")
        self.ignore("fi.epitest.hostap.WPASupplicant.Interface", "ScanResultsAvailable")
        self.ignore("com.redhat.PrinterSpooler", "QueueChanged")
        self.ignore("org.freedesktop.NetworkManager", "StateChange") # deprecated
        self.watch(self.nm_sc_h, "org.freedesktop.NetworkManager", "StateChanged")
        self.watch(self.wpas_isc_h, "fi.epitest.hostap.WPASupplicant.Interface", "StateChange")
        self.watch(self.nmd_sc_h, "org.freedesktop.NetworkManager.Device", "StateChanged")
        self.watch(self.bus_noc_h, "org.freedesktop.DBus", "NameOwnerChanged")

    def bus_noc_h(self, *args, **kwargs):
        (name, old, new) = args
        if new == "":
            new = "gone"
        else:
            new = "at " + new
        print "\tBUS NOC\t%s %s" % (name, new)

    def wpas_isc_h(self, *args, **kwargs):
        opath = kwargs["path"]
        (new, old) = args
        print "\tWPAS %s\t(%s, was %s)" % (new, opath, old.lower())

    def nmd_sc_h(self, *args, **kwargs):
        opath = kwargs["path"]
        (new, old, reason) = args
        news = cDevice_07.NM_DEVICE_STATE[new]
        olds = cDevice_07.NM_DEVICE_STATE[old]
        reasons = ""
        if reason != 0:
            reasons = "reason %d" % reason
        print "\tDevice State %s\t(%s, was %s%s)" % (news, opath, olds.lower(), reasons)

    def nm_sc_h(self, *args, **kwargs):
        s = args[0]
        ss = cNM.NM_STATE[s]
        print "\tNM State:", ss

    def propc_h(self, *args, **kwargs):
        opath = kwargs["path"]
        props = args[0]
        for k, v in props.iteritems():
            if k == "Strength":
                v = "%u" % v
            line = "\tPROP\t%s\t%s\t(%s)" % (k, v, opath)
            print line


# main

op = OptionParser(version="%prog " + VERSION)
op.add_option("-d", "--dev",
              action="store_true", default=False,
              help="list devices")
op.add_option("-c", "--actcon",
              action="store_true", default=False,
              help="list active connections")
op.add_option("-u", "--usrcon",
              action="store_true", default=False,
              help="list user connection settings (can CRASH nm-applet)")
op.add_option("-s", "--syscon",
              action="store_true", default=False,
              help="list system connection settings")
op.add_option("-a", "--ap",
              action="store_true", default=False,
              help="list found access points")
op.add_option("-n", "--nets",
              action="store_true", default=False,
              help="list found wireless networks")
# TODO http://docs.python.org/lib/optparse-adding-new-types.html
op.add_option("-w", "--wifi",
              choices=["0","1","off","on","no","yes","false","true"],
              metavar="BOOL",
              help="enable or disable wireless")
op.add_option("-o", "--online",
              choices=["0","1","off","on","no","yes","false","true"],
              metavar="BOOL",
              help="enable or disable network at all")

op.add_option("-C", "--connect",
              help="connect to a wireless network NET (using knetworkmanagerrc or the key options below)",
              metavar="NET")
op.add_option("--unprotected",
              action="store_true", default=False,
              help="network does not require a key")
op.add_option("--wep-hex",
              metavar="KEY",
              help="use this WEP key of 26 hex digits")
op.add_option("--wep-pass",
              metavar="KEY",
              help="use this WEP passphrase")
op.add_option("--wpa-psk-hex",
              metavar="KEY",
              help="use this WPA key of 64 hex digits")
op.add_option("--wpa-pass",
	      metavar="KEY",
	      help="use this WPA passphrase")
op.add_option("-m", "--monitor",
              action="store_true", default=False,
              help="loop to show dbus signals")


(options, args) = op.parse_args()

if options.ap:
    options.dev = True
if options.monitor:
    LOOP = True


nmp = '/org/freedesktop/NetworkManager'
try:
    nm = make_nm(nmp)
except dbus.exceptions.DBusException, e:
    print e
    print "NetworkManager is not running"
    sys.exit(1)
if options.dev or options.actcon:
    nm.Dump()

true_choices =  ["1", "on", "yes", "true"]
if options.wifi != None:
    nm.SetWifiEnabled(options.wifi in true_choices)
if options.online != None:
    nm.SetOnline(options.online in true_choices)

if options.nets:
    nm.ListNets()

if options.syscon:
    print "SYSTEM Connections"
    if nm.Api() == "06":
        print "Cannot do that with NM 0.6"
    else:
        ss = cApplet(SSC, '/org/freedesktop/NetworkManagerSettings')
        ss.Dump()

if options.usrcon:
    print "USER Connections"
    try:
        if nm.Api() == "06":
            us = cApplet_06(NMIC, "/org/freedesktop/NetworkManagerInfo")
        else:
            us = cApplet(USC, '/org/freedesktop/NetworkManagerSettings')
        us.Dump()
    except dbus.exceptions.DBusException, e:
        print e
        #if e.get_dbus_name() == "org.freedesktop.DBus.Error.ServiceUnknown":
        print "Applet is not running"

nmo = bus.get_object(NMC, nmp)
nmi = dbus.Interface(nmo, NMI)

def service_pid(name):
    DBS = 'org.freedesktop.DBus'
    DBI = DBS
    dbo = bus.get_object(DBS, '/')
    dbi = dbus.Interface(dbo, DBI)
    owner = dbi.GetNameOwner(name)
    pid = dbi.GetConnectionUnixProcessID(owner)
    return pid

# TODO UserSettings_06
if options.connect != None:
    if nm.Api() == "06":
        name = NMIC
    else:
        name = USC
    brn = bus.request_name(name, _dbus_bindings.NAME_FLAG_DO_NOT_QUEUE)
    if brn == _dbus_bindings.REQUEST_NAME_REPLY_EXISTS:
        print "Could not provide settings service, another applet is running (pid %s)" % service_pid(name)
        sys.exit(1)
    cfg = ConfigParserKNM()
    if nm.Api() == "06":
        us = UserSettings_06("/org/freedesktop/NetworkManagerInfo",
                             cfg.ConMaps())
    else:
        us = UserSettings("/org/freedesktop/NetworkManagerSettings",
                      cfg.ConMaps())

def Connect(wanted_net):  # any. or take arg. net is config name or ssid name
    #  ... in general, look for string in all config data. ssid for wifi, whatever for dialup
    # TODO also respect autoconnect

    # ActivateConn wants setting device ap; can find device from ap? ap is "specific" for wifi devices
    #print "Connection wanted to", wanted_net
    found_con = found_ap = found_dev = None
    for dev in nm.Devices():
	for ap in dev.APs():
	    if wanted_net == ap.Ssid():
		found_ap = ap
		found_dev = dev
		break # FIXME both loops
    found_con = us.GetByNet(wanted_net)
    if found_ap == None:
	print "No AP found with SSID", wanted_net
        return False
    if found_con == None:
        print "No settings for net %s, assuming no key is needed" % wanted_net
        c = mkconmap_wifi(wanted_net)
        found_con = us.addCon(c)
    nm.ActivateConnection(found_con, found_dev, found_ap) # TODO async
    # TODO run loop, exit it when we have serviced the required calls
    return True

if options.connect != None:
    if options.unprotected:
        c = mkconmap_wifi(options.connect)
        us.addCon(c)
    if options.wep_hex != None:
        c = mkconmap_wep(options.connect, options.wep_hex)
        us.addCon(c)
    if options.wep_pass != None:
        c = mkconmap_wep_pass(options.connect, options.wep_pass)
        us.addCon(c)
    if options.wpa_psk_hex != None:
        c = mkconmap_psk(options.connect, options.wpa_psk_hex)
        us.addCon(c)
    if options.wpa_pass != None:
	wpa_psk_hex  = hexlify(pbkdf2.pbkdf2(options.wpa_pass, options.connect, 4096, 32))
        print "pbkdf2", wpa_psk_hex
	c = mkconmap_psk(options.connect, wpa_psk_hex)
        us.addCon(c)
    nm.WatchState()
    if Connect(options.connect):
        LOOP = True

if options.monitor:
    m = Monitor()

def loop():
    loop = gobject.MainLoop()
    try:
        loop.run()
    except:
        print "Loop exited"

if LOOP:
    loop()
