From 38f7b126ff346c8c6cb1820293d466adfab0a086 Mon Sep 17 00:00:00 2001 From: Joe Campbell <10763991+SgtReckless@users.noreply.github.com> Date: Thu, 25 Mar 2021 18:02:50 +0000 Subject: [PATCH] Windows optimisations --- README.md | 9 +- changelog.txt | 11 +++ wifipasswords/__init__.py | 11 ++- wifipasswords/wifipasswords_linux.py | 11 +++ wifipasswords/wifipasswords_windows.py | 110 ++++++++++++++----------- 5 files changed, 99 insertions(+), 53 deletions(-) diff --git a/README.md b/README.md index 26e0f0f..4289c40 100644 --- a/README.md +++ b/README.md @@ -32,9 +32,10 @@ Usage from wifipasswords import WifiPasswords passwords = WifiPasswords().get_passwords() -connected_ssids = WifiPasswords().get_currently_connected_ssids() +connected_passwords = WifiPasswords().get_currently_connected_passwords() print(passwords) +print(connected_passwords) WifiPasswords().save_wpa_supplicant('.', passwords, True, 'GB') ``` @@ -67,14 +68,14 @@ To-Do - [X] Add getters for accessing variables directly - [ ] Fix visible network, DNS config and number of interfaces for Linux - [ ] Add automated tests -- [ ] Add method to check and return only the connected SSID name and Password +- [X] Add method to check and return only the connected SSID name and Password - [ ] Use nmcli to retrieve passwords on linux rather than reading files (may not require sudo) - +- [X] Multithreading support for windows to imporove execution speed About ----- Creation date: 10-02-2019 -Modified date: 23-03-2021 +Modified date: 25-03-2021 Dependencies: colorama diff --git a/changelog.txt b/changelog.txt index b59dc02..b822bef 100644 --- a/changelog.txt +++ b/changelog.txt @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + +## 0.3.3b - 25-03-2021 +### Added +- added get_currently_connected_passwords() to return current connected SSID and psk only +### Changed +- subprocess command calls split out into separate function for code clarity/efficiency +- Multithreading support in windows to get_passwords() -> execution time reduced from 2.1s to 0.6s for 24 networks + + ## 0.3.2b - 24-03-2021 ### Added - Getters for accessing variables directly from WifiPasswords class @@ -11,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - fixed import issue in wifipasswords_exe - fixed key error in get_passwords_dummy() + ## 0.3.1b - 23-03-2021 ### Added - Added support to detect mac-randomisation diff --git a/wifipasswords/__init__.py b/wifipasswords/__init__.py index 823beb4..23169d7 100644 --- a/wifipasswords/__init__.py +++ b/wifipasswords/__init__.py @@ -7,7 +7,7 @@ Uses the netsh windows module. Pass --JSON argument to export as JSON. Pass --wpasupplicant to create a wpa_supplicant.conf file for linux Creation date: 10-02-2019 - Modified date: 24-03-2021 + Modified date: 25-03-2021 Dependencies: colorama """ __copyright__ = "Copyright (C) 2019-2021 Joe Campbell" @@ -25,7 +25,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see < https: // www.gnu.org/licenses/>. -__version__ = "0.3.2-beta" +__version__ = "0.3.3-beta" __licence__ = "GPLv3" # GNU General Public Licence v3 import platform @@ -190,3 +190,10 @@ def get_currently_connected_ssids(self) -> list: checks all active interfaces on the system. \n """ return self._WifiPasswordsSubclass.get_currently_connected_ssids() + + + def get_currently_connected_passwords(self) -> list: + """ + Returns a tuple of (ssid, psk) for each currently connected network as a list. + """ + return self._WifiPasswordsSubclass.get_currently_connected_passwords() diff --git a/wifipasswords/wifipasswords_linux.py b/wifipasswords/wifipasswords_linux.py index a0338e5..6a1fe33 100644 --- a/wifipasswords/wifipasswords_linux.py +++ b/wifipasswords/wifipasswords_linux.py @@ -100,6 +100,9 @@ def get_passwords(self) -> dict: network_blocks = re.findall('(?<=network=)[^}]*(?=})',file_string) for network_block in network_blocks: block_stripped = network_block.strip().replace('\t','').replace('\n',' ').split(' ') + ssid = ' ' + auth = ' ' + psk = ' ' for item in block_stripped: if 'ssid' in item: ssid = item.split('ssid=')[1][1:-1] @@ -267,3 +270,11 @@ def get_currently_connected_ssids(self) -> list: connected_ssids.append(item) return connected_ssids + + ## + ## to be implemented + def get_currently_connected_passwords(self) -> list: + """ + Returns a tuple of (ssid, psk) for each currently connected network. + """ + return [('','')] diff --git a/wifipasswords/wifipasswords_windows.py b/wifipasswords/wifipasswords_windows.py index a656e47..51876fa 100644 --- a/wifipasswords/wifipasswords_windows.py +++ b/wifipasswords/wifipasswords_windows.py @@ -1,6 +1,6 @@ # windows specific version of class # imported as subclass to main WifiPasswords class in __init__ -# functions are 1:1 maapping of stub funcitons in WifiPasswords with platform specific code +# exposed functions are 1:1 maapping of stub funcitons in WifiPasswords with platform specific code # documentation for funcitons provided only in main __init__ WifiPasswords class as is the only class designed to be exposed __copyright__ = "Copyright (C) 2019-2021 Joe Campbell" @@ -22,10 +22,10 @@ import os import json import re +from multiprocessing.dummy import Pool as ThreadPool from . import __version__, __copyright__, __licence__ - class WifiPasswordsWindows: def __init__(self) -> None: self.data = {} @@ -35,41 +35,60 @@ def __init__(self) -> None: self.net_template = {'auth': '', 'psk': '', 'metered': False, 'macrandom': 'Disabled'} - def get_passwords(self) -> dict: + + @staticmethod + def _command_runner(shell_commands:list) -> str: + """ + Split subprocess calls into separate runner module for clarity of code.\n + Takes the command to execute as a subprocess in the form of a list.\n + Returns the string output as a utf-8 decoded output.\n + """ # need to use pipes for all STDIO on windows if running without interactive console. # STARTUPINFO is only present on windows, not linux si = subprocess.STARTUPINFO() si.dwFlags |= subprocess.STARTF_USESHOWWINDOW - profiles_list = subprocess.run(['netsh', 'wlan', 'show', 'profiles'], + return_data = subprocess.run(shell_commands, stdout=subprocess.PIPE, stderr=subprocess.PIPE, stdin=subprocess.PIPE, - startupinfo=si).stdout.decode('utf-8').split('\r\n') + startupinfo=si).stdout.decode('utf-8') + return return_data - networks = {(row.split(': ')[1]): self.net_template.copy() - for row in profiles_list if "User Profile" in row} - for net, value in networks.items(): - profile_info = subprocess.run(['netsh', 'wlan', 'show', 'profile', net, 'key=clear'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - stdin=subprocess.PIPE, - startupinfo=si).stdout.decode('utf-8').split('\r\n') + def _get_password_subthread(self,network): + # network is a tuple from the networks dictionary + # values are (ssid, value dictionary) + profile_info = self._command_runner( + ['netsh', 'wlan', 'show', 'profile', network[0], 'key=clear']).split('\r\n') - for row in profile_info: - if "Key Content" in row: - value['psk'] = row.split(': ')[1].strip() - if "Authentication" in row: - value['auth'] = row.split(': ')[1].strip() - if "Cost" in row: - if "Fixed" in row or "Variable" in row: - value['metered'] = True - if "MAC Randomization" in row: - value['macrandom'] = row.split(': ')[1].strip() + for row in profile_info: + if "Key Content" in row: + network[1]['psk'] = row.split(': ')[1].strip() + if "Authentication" in row: + network[1]['auth'] = row.split(': ')[1].strip() + if "Cost" in row: + if "Fixed" in row or "Variable" in row: + network[1]['metered'] = True + if "MAC Randomization" in row: + network[1]['macrandom'] = row.split(': ')[1].strip() + return network - self.number_of_profiles = len(networks) - self.data = networks - return networks + + def get_passwords(self) -> dict: + profiles_list = self._command_runner( + ['netsh', 'wlan', 'show', 'profiles']).split('\r\n') + + networks = {(row.split(': ')[1]): self.net_template.copy() + for row in profiles_list if "Profile :" in row} + + # from testing 6 seems the optimum thread number + pool = ThreadPool(6) + results = dict(pool.imap(self._get_password_subthread,networks.items())) + pool.close() + pool.join() + self.number_of_profiles = len(results) + self.data = results + return results def get_passwords_dummy(self, delay: float = 0.5, quantity: int = 10) -> dict: @@ -98,13 +117,7 @@ def get_passwords_data(self) -> dict: def get_visible_networks(self, as_dictionary=False) -> str: - si = subprocess.STARTUPINFO() - si.dwFlags |= subprocess.STARTF_USESHOWWINDOW - current_networks = subprocess.run(['netsh', 'wlan', 'show', 'networks', 'mode=Bssid'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - stdin=subprocess.PIPE, - startupinfo=si).stdout.decode('utf-8') + current_networks = self._command_runner(['netsh', 'wlan', 'show', 'networks', 'mode=Bssid']) if "powered down" in current_networks: self.number_visible_networks = 0 @@ -161,13 +174,7 @@ def get_visible_networks(self, as_dictionary=False) -> str: def get_dns_config(self, as_dictionary=False) -> str: - si = subprocess.STARTUPINFO() - si.dwFlags |= subprocess.STARTF_USESHOWWINDOW - dns_settings = subprocess.run(['netsh', 'interface', 'ip', 'show', 'dns'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - stdin=subprocess.PIPE, - startupinfo=si).stdout.decode('utf-8') + dns_settings = self._command_runner(['netsh', 'interface', 'ip', 'show', 'dns']) split_dns_config = dns_settings.strip().split('\r\n\r\n') self.number_of_interfaces = len(split_dns_config) @@ -258,14 +265,8 @@ def get_number_profiles(self) -> int: def get_currently_connected_ssids(self) -> list: connected_ssids = [] + current_interfaces = self._command_runner(['netsh', 'wlan', 'show', 'interfaces']).split('\r\n') - si = subprocess.STARTUPINFO() - si.dwFlags |= subprocess.STARTF_USESHOWWINDOW - current_interfaces = subprocess.run(['netsh', 'wlan', 'show', 'interfaces'], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - stdin=subprocess.PIPE, - startupinfo=si).stdout.decode('utf-8').split('\r\n') for line in current_interfaces: # space before ssid prevents BSSID being captured if " SSID" in line: @@ -273,3 +274,18 @@ def get_currently_connected_ssids(self) -> list: connected_ssids.append(line.split(':')[1].strip()) return connected_ssids + + + def get_currently_connected_passwords(self) -> list: + connected_passwords = [] + connected_ssids = self.get_currently_connected_ssids() + + for ssid in connected_ssids: + key_data = self._command_runner(['netsh', 'wlan', 'show', 'profile', ssid, 'key=clear']).split('\r\n') + psk = '' + for row in key_data: + if "Key Content" in row: + psk = row.split(': ')[1].strip() + connected_passwords.append((ssid,psk)) + + return connected_passwords \ No newline at end of file