WLAN gehilfe

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
Benutzeravatar
Domroon
User
Beiträge: 104
Registriert: Dienstag 3. November 2020, 10:27
Wohnort: Dortmund

Hallo Leute,

ich wechsle ständig mit meinem Laptop den Ort und nutze deshalb drei verschiedene WLAN-Netzwerke.
Auch wenn Windows 10 von sich aus die Netze verwaltet, hatte ich die Lust ein eigenes Programm zu schreiben, welche diese verwaltet.

Ich habe das Programm so programmiert, sodass es als ein klassiches Kommandozeilenprogramm (wie man es bei Linux gewöhnt ist) benutzt werden kann.
Somit ist es möglich Sub-Befehle und Parameter an das Programm zu übergeben, indem man die richtigen Begriffe einfach hinter den Namen des Scriptes
schreibt, wenn man dieses in der Kommandozeile öffnet.

Wenn ihr mein Programm zum Beispiel "wlanHelper.py" nennt dann kann folgendes gemacht werden.
Die Eingabe von:

Code: Alles auswählen

wlanHelper.py -h
Gibt euch folgenden Hilfetext zurück:

Code: Alles auswählen

usage: wlanHelper.py [-h] {connect,connect-loop,show,show-stored,add,delete} ...

This program connects the current device to the saved wireless networks. The ranking corresponds to the order in which the networks are stored.

positional arguments:
  {connect,connect-loop,show,show-stored,add,delete}
                        sub-commands
    connect             connect the computer
    connect-loop        connect and reconnect the computer if it is disconnected.
    show                show available wifi networks near you
    show-stored         show wifi networks that you have already stored
    add                 save a network
    delete              delete a saved network

options:
  -h, --help            show this help message and exit

Um ein Netzwerk abzuspeichern (hier mit einem Key, da dieses verschlüsselt ist) müsst ihr also folgendes tun:

Code: Alles auswählen

wlanHelper.py add TestNetwerk SuperGeheimesKennwort
Um euch mit den abgespeicherten Netzwerken zu verbinden könnt ihr wahlweise den Sub-Befehl "connect" oder "connect-loop" verwenden.

Um das Programm benutzen zu können braucht einige Module die ihr mit folgendem Befehl herunterladen könnt:

Code: Alles auswählen

pip install cffi comtypes cryptography pycparser pywifi
Nun folgt mein Programm, welche bisher nur unter Windows 10 getestet wurde.
Ich werde das Programm in kürze auch auf Linux in der Variante "Debian" testen.

Der "SECRET_KEY" ist ab sofort natürlich nicht mehr so "secret". Daher empfiehlt es sich, einen eigenen Schlüssel zu erzeugen.
Hier wird beschrieben wie es geht: https://pypi.org/project/cryptography/

Den Secret-Key mit in der Konfigurationsdatei zu speichern wäre eigentlich komfortabler aber irgendwie auch sinnlos, weil man dann
das verschlüsseln der WLAN-Keys auch direkt sein lassen kann :D

Für Fragen, Anmerkungen und Kritik bin ich offen und dankbar :wink:

wlanHelper.py:

Code: Alles auswählen

from time import sleep
import argparse
import configparser

import pywifi
from cryptography.fernet import Fernet


INTERFACE_STATUS = [
    "disconnected",
    "scanning",
    "inactive",
    "connecting",
    "connected"
]
SECRET_KEY = b'Qu099307GwgscI9IGGdHOa1r97aXCABiDceb6P3kY_Y='


def create_profile(ssid, key=None):
    profile = pywifi.Profile()
    profile.ssid = ssid
    profile.auth = pywifi.const.AUTH_ALG_OPEN
    if key is not None:
        profile.akm.append(pywifi.const.AKM_TYPE_WPA2PSK)
        profile.cipher = pywifi.const.CIPHER_TYPE_CCMP
        profile.key = key
    else:
        profile.akm.append(pywifi.const.AKM_TYPE_NONE)
        
    return profile


def connect(profile, interface):
    profile = interface.add_network_profile(profile)
    interface.connect(profile)
    for _ in range(10):
        status = INTERFACE_STATUS[interface.status()]
        if status == 'connected':
            print(f'connected with "{profile.ssid}"')
            break
        elif status == 'connecting':
            print(f'connecting to "{profile.ssid}"')
        else:
            print(status)
        sleep(1)
        
        
def get_network_names(interface):
    interface.scan()
    print('scanning for networks\n')
    sleep(2)
    scanned_profiles = interface.scan_results()
    ssids = []
    for profile in scanned_profiles:
        ssids.append(profile.ssid)
    return set(ssids)


def get_prog_args():
    parser = argparse.ArgumentParser(description='This program connects the current device to the saved wireless networks. The ranking corresponds to the order in which the networks are stored.')

    subparsers = parser.add_subparsers(help='sub-commands', dest='sub')
    connect_parser = subparsers.add_parser('connect', help='connect the computer')

    connect_loop_parser = subparsers.add_parser('connect-loop', help='connect and reconnect the computer if it is disconnected.')

    show_parser = subparsers.add_parser('show', help='show available wifi networks near you')

    show_stored_parser = subparsers.add_parser('show-stored', help='show wifi networks that you have already stored')

    add_parser = subparsers.add_parser('add', help='save a network')
    add_parser.add_argument('ssid', type=str, help='networkname that you want to add to your connection list')
    add_parser.add_argument('key', nargs='?', type=str, help='wpa2-key for the network')

    delete_parser = subparsers.add_parser('delete', help='delete a saved network')
    delete_parser.add_argument('ssid', type=str, help='networkname that you want to delete from your connection list')

    return parser.parse_args()


def store_network(ssid, config, key=None):
    config_list = config.read('networks.ini')
    if not config_list:
        print('no config file')
        print('create config file')
        config['Network 1'] = {'ssid': ssid}
        if key:
            config['Network 1']['key'] = key
        with open('networks.ini', 'w') as configfile:
            config.write(configfile)
    else:
        network_qty = len(config.sections())
        network_identifier = f'Network {network_qty + 1}'
        config[network_identifier] = {'ssid': ssid}
        if key:
            config[network_identifier]['key'] = key
        with open('networks.ini', 'w') as configfile:
            config.write(configfile)


def get_stored_networks(config):
    config.read('networks.ini')
    stored_networks = []
    for network in config.sections():
        stored_network = {}
        stored_network['network'] = config[network].name
        stored_network['ssid'] = config[network]['ssid']
        try:
            stored_network['key'] = config[network]['key']
        except KeyError:
            stored_network['key'] = None
        stored_networks.append(stored_network)
    return stored_networks


def connect_wisely(interface, config, fernet):
    stored_networks = get_stored_networks(config)
    available_networks = get_network_names(interface)
    
    for stored_network in stored_networks:
        if stored_network['ssid'] in available_networks:
            print(f'Found "{stored_network["network"]}" with SSID "{stored_network["ssid"]}"')
            stored_key = stored_network['key']
            if stored_key:
                encoded_key = stored_key.encode()
                key = fernet.decrypt(encoded_key).decode()
                profile = create_profile(stored_network['ssid'], key=key)
            else:
                profile = create_profile(stored_network['ssid'])
            connect(profile, interface)
            return True
            

def delete_stored_network(ssid, config):
    config_list = config.read('networks.ini')
    stored_networks = get_stored_networks(config)
    is_stored = False
    for network in stored_networks:
        if network['ssid'] == ssid:
            config.remove_section(f'{network["network"]}')
            with open('networks.ini', 'w') as configfile:
                config.write(configfile)
            is_stored = True
            print(f'deleted network with SSID "{ssid}"')
            break
    if not is_stored:
        print(f'Could not find a stored network with the SSID "{ssid}"')


def main():
    args = get_prog_args()
    wifi = pywifi.PyWiFi()
    interface = wifi.interfaces()[0]
    config = configparser.ConfigParser()
    fernet = Fernet(SECRET_KEY)
    
    if args.sub == 'connect':
        print(INTERFACE_STATUS[interface.status()])
        connected = connect_wisely(interface, config, fernet)
        if not connected:
            print('Could not find a available network that you have stored.')
    elif args.sub == 'connect-loop':
        print(INTERFACE_STATUS[interface.status()])
        while True:
            connect_wisely(interface, config, fernet)
            while True:
                if INTERFACE_STATUS[interface.status()] == 'disconnected':
                    print('disconnected')
                    break
                sleep(5)
    elif args.sub == 'show':
        for name in get_network_names(interface):
            print(name)
    elif args.sub == 'show-stored':
        for stored_network in get_stored_networks(config):
            print(stored_network['ssid'])
    elif args.sub == 'add':
        if args.key:
            encrypted_key = fernet.encrypt(args.key.encode())
            store_network(args.ssid, config, key=encrypted_key.decode())
            print(f'add network "{args.ssid}" with key "{args.key}"')
        else:
            store_network(args.ssid, config)
            print(f'add opened network "{args.ssid}" without key')
    elif args.sub == 'delete':
        delete_stored_network(args.ssid, config)


if __name__ == '__main__':
    main()
Ihr findet das ganze auch in meinem Github - Repository: https://github.com/Domroon/wlan_python
__deets__
User
Beiträge: 14529
Registriert: Mittwoch 14. Oktober 2015, 14:29

Das ist leider eine schlechte Idee, denn der secret key ist ja auf dem gleichen System zu finden, auf dem auch die damit verschluesselten Daten liegen. Damit ist derjenige, der Zugriff auf die einen Daten bekommt, auch trivial im Besitz der anderen. Das ist also ein massives Sicherheitsleck.

Wenn man sowas vernuenftig machen will, gibt es mehrere Moeglichkeiten:

- jede Benutzung erfordert die Eingabe eines Masterpassworts, womit die zur Entschluesselung notwendigen Daten nur kurz im Speicher des Programms vorliegen, waehrend es laeuft.
- man benutzt einen vom System bereitgestellten KeyChain-Mechanismus. KDE hat zB KWallet, macOS eine keychain. Windows hat sowas bestimmt auch, aber ich kenne das nicht. Damit kannst du mindestens sicherstellen, dass die Daten nicht abgegriffen werden koennen, wenn jemand physischen Zugriff auf die Platte hat, aber das System nicht laeuft.
Benutzeravatar
__blackjack__
User
Beiträge: 13077
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@Domroon: Beim erstellen der Argumentparser wird einiges an Namen gebunden die dann nirgends verwendet werden.

Die `INTERFACE_STATUS`-Liste könnte man als `Enum` definieren die man aus den Konstanten `pywifi.const.IFACE_*` erstellt.

Das mit dem `ConfigParser` der als Argument übergeben wird und wo dann *in* den Funktionen `read()` aufgerufen wird, ist keine so gute Idee, weil das nicht zum komplett neu einlesen führt, sondern nur zum aktualisieren in der Datei vorhandener Werte. Wenn in der Datei Schlüssel gelöscht wurden, dann verschwinden die durch `read()` nicht aus dem bestehenden Objekt. Wahrscheinlich ist das kein Problem das durch die Aufrufe aus der `main()` relevant wäre, aber grundsätzlich gäbe es dieses Problem. Das lesen würde ich also entweder schon in der `main()` machen, oder aber das Objekt in den einzelnen Funktionen wo jetzt gelesen wird, frisch erstellen.

Der Dateiname "networks.ini" sollte nur einmal im Quelltext stehen, damit man den leicht ändern kann, ohne das man den Fehler machen kann irgendeine Stelle zu vergessen oder einen Typfehler zu machen.

`connect_wisely()` gibt entweder explizit `True` oder implizit `None` zurück. Das sollte beides explizit sein, und `False` statt `None`.

Das Ergebnis in `get_network_names()` wird ziemlich ”wortreich” erstellt und mit einer unnötigen Liste als Zwischenergebnis. Die letzten 5 Zeilen wären *eine* „set comprehension“.

Wobei ein `set()` hier vielleicht keine so gute Entscheidung ist. Ein Aurufer hat dadurch einen Vorteil weil er effizient mit ``in`` prüfen kann, aber der andere Aufrufer gibt die Namen aus und zwar in einer ”zufälligen” Reihenfolge die sich selbst bei den gleichen Netzwerknamen bei wiederholtem "show"-Kommando ändern kann. Ich würde da entweder die Reihenfolge vom Scan-Ergebnis beibehalten und eine Liste (oder einen Iterator) zurückgeben, oder beim "schow"-Kommando die Netzwerknamen explizit sortieren.

In `get_stored_networks()` ist ``config[network].name`` etwas umständlich denn das ist ja immer `network`.

`stored_network` kann man direkt mit einem Wörterbuchliteral schreiben. Die Ausnahmebehandlung braucht man nicht, weil `Section`-Objekte wie Wörterbücher eine `get()`-Methode haben. Dann wird die Schleife so einfach, dass man eine „list comprehension“ daraus machen kann.

Das ``if``/``else`` bei `store_network()` behandelt nicht wirklich einen Unterschied weil der ``else``-Zweig auch für eine leere oder vorher nicht existente Konfigurationsdatei funktioniert.

Das heisst eigentlich funktioniert das insgesamt nicht wirklich. Stell Dir vor Du fügst zwei Einträge hinzu, die haben dann die Abschnittsnamen "Network 1" und "Network 2". Jetzt löschst Du "Network 1". Es bleibt nur noch "Network 2". Nun fügst Du wieder einen Eintrag hinzu: Und schon wird der vorhandene Abschnitt "Network 2" überschrieben.

Was die Funktion auch nicht verhindert oder behandelt ist das hinzufügen von mehreren Einträgen mit der selben SSID.

Es würde hier also Sinn machen nicht einen künstlichen Abschnittsnamen zu generieren, sondern die SSID als Abschnittsnamen zu verwenden. Der Code für das löschen eines Eintrags wird dann auch bedeutend einfacher.

Zwischenstand (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
import argparse
import configparser
from enum import Enum
from time import sleep

import pywifi
from cryptography.fernet import Fernet

INTERFACE_STATUS_PREFIX = "IFACE_"
InterfaceStatus = Enum(
    "InterfaceStatus",
    [
        (name[len(INTERFACE_STATUS_PREFIX) :], value)
        for name, value in vars(pywifi.const).items()
        if name.startswith(INTERFACE_STATUS_PREFIX)
    ],
)

SECRET_KEY = b"Qu099307GwgscI9IGGdHOa1r97aXCABiDceb6P3kY_Y="

NETWORKS_FILENAME = "networks.ini"


def create_profile(ssid, key=None):
    profile = pywifi.Profile()
    profile.ssid = ssid
    profile.auth = pywifi.const.AUTH_ALG_OPEN
    if key is not None:
        profile.akm.append(pywifi.const.AKM_TYPE_WPA2PSK)
        profile.cipher = pywifi.const.CIPHER_TYPE_CCMP
        profile.key = key
    else:
        profile.akm.append(pywifi.const.AKM_TYPE_NONE)

    return profile


def get_status(interface):
    return InterfaceStatus(interface.status())


def get_network_names(interface):
    interface.scan()
    print("scanning for networks\n")
    sleep(2)
    return {profile.ssid for profile in interface.scan_results()}


def connect(profile, interface):
    profile = interface.add_network_profile(profile)
    interface.connect(profile)
    for _ in range(10):
        status = get_status(interface)
        if status is InterfaceStatus.CONNECTED:
            print(f"connected with '{profile.ssid}'")
            break

        if status is InterfaceStatus.CONNECTING:
            print(f"connecting to '{profile.ssid}'")
        else:
            print(status.name.lower())
        sleep(1)


def get_stored_networks(config):
    return [
        {"ssid": section.name, "key": section.get("key")}
        for section in (config[name] for name in config.sections())
    ]


def connect_wisely(interface, config, fernet):
    available_networks = get_network_names(interface)
    for stored_network in get_stored_networks(config):
        ssid = stored_network["ssid"]
        if ssid in available_networks:
            print(f"Found network with SSID '{ssid}'")
            stored_key = stored_network["key"]
            connect(
                create_profile(
                    ssid,
                    key=fernet.decrypt(stored_key.encode()).decode()
                    if stored_key
                    else None,
                ),
                interface,
            )
            return True

    return False


def store_network(ssid, config, key=None):
    config.add_section(ssid)
    if key:
        config[ssid]["key"] = key

    with open(NETWORKS_FILENAME, "w", encoding="utf8") as file:
        config.write(file)


def delete_stored_network(ssid, config):
    if config.remove_section(ssid):
        with open(NETWORKS_FILENAME, "w", encoding="utf8") as file:
            config.write(file)
        print(f"deleted network with SSID '{ssid}'")
    else:
        print(f"Could not find a stored network with the SSID '{ssid}'")


def get_command_line_arguments():
    parser = argparse.ArgumentParser(
        description=(
            "This program connects the current device to the saved wireless"
            " networks. The ranking corresponds to the order in which the"
            " networks are stored."
        )
    )
    subparsers = parser.add_subparsers(help="commands", dest="command")

    subparsers.add_parser("connect", help="connect the computer")
    subparsers.add_parser(
        "connect-loop",
        help="connect and reconnect the computer if it is disconnected.",
    )
    subparsers.add_parser("show", help="show available wifi networks near you")
    subparsers.add_parser(
        "show-stored", help="show wifi networks that you have already stored"
    )

    add_parser = subparsers.add_parser("add", help="save a network")
    add_parser.add_argument(
        "ssid", help="networkname that you want to add to your connection list"
    )
    add_parser.add_argument("key", nargs="?", help="wpa2-key for the network")

    delete_parser = subparsers.add_parser(
        "delete", help="delete a saved network"
    )
    delete_parser.add_argument(
        "ssid",
        help="networkname that you want to delete from your connection list",
    )

    return parser.parse_args()


def main():
    arguments = get_command_line_arguments()
    interface = pywifi.PyWiFi().interfaces()[0]
    config = configparser.ConfigParser()
    config.read(NETWORKS_FILENAME, encoding="utf8")
    fernet = Fernet(SECRET_KEY)

    if arguments.command == "connect":
        print(get_status(interface).name.lower())
        if not connect_wisely(interface, config, fernet):
            print("Could not find a available network that you have stored.")

    elif arguments.command == "connect-loop":
        print(get_status(interface).name.lower())
        while True:
            connect_wisely(interface, config, fernet)
            while True:
                if get_status(interface) is InterfaceStatus.DISCONNECTED:
                    print("disconnected")
                    break
                sleep(5)

    elif arguments.command == "show":
        for name in sorted(get_network_names(interface)):
            print(name)

    elif arguments.command == "show-stored":
        for stored_network in get_stored_networks(config):
            print(stored_network["ssid"])

    elif arguments.command == "add":
        if arguments.key:
            store_network(
                arguments.ssid,
                config,
                fernet.encrypt(arguments.key.encode()).decode(),
            )
            print(f"add network '{arguments.ssid}' with key '{arguments.key}'")
        else:
            store_network(arguments.ssid, config)
            print(f"add opened network '{arguments.ssid}' without key")

    elif arguments.command == "delete":
        delete_stored_network(arguments.ssid, config)


if __name__ == "__main__":
    main()
Im nächsten Schritt würde ich dann zwei bis drei Klassen einführen. Eine Datenklasse statt der Wörterbücher für ein einzelnes `StoredNetwork`, dann eine Wrapperklasse für den `ConfigParser` den man beispielsweise `NetworkStore` nennen könnte, und wahrscheinlich eine Wrapperklasse für das WiFi-Interface.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten