├── MANIFEST.in ├── etc ├── nose.cfg └── coverage.cfg ├── docs ├── source │ ├── usage_cli.rst │ ├── usage_library.rst │ ├── api.rst │ ├── tips_and_tricks.rst │ ├── index.rst │ └── conf.py ├── Makefile └── make.bat ├── dev-requirements.txt ├── run_tests.sh ├── wpa_supplicant ├── test │ ├── test_cli.py │ ├── __init__.py │ ├── mocks.py │ └── test_core.py ├── __init__.py ├── examples │ ├── example_scan.py │ └── example_txdbus.py ├── cli.py └── core.py ├── tox.ini ├── .travis.yml ├── CHANGELOG.md ├── .codeclimate.yml ├── .gitignore ├── HACKING.md ├── feature_map.csv ├── toxtest.sh ├── setup.py ├── README.md └── LICENSE /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include README.rst 3 | -------------------------------------------------------------------------------- /etc/nose.cfg: -------------------------------------------------------------------------------- 1 | [nosetests] 2 | tests = wpa_supplicant/test/ -------------------------------------------------------------------------------- /docs/source/usage_cli.rst: -------------------------------------------------------------------------------- 1 | Using the wpa_supplicant CLI 2 | ============================ 3 | 4 | TODO 5 | -------------------------------------------------------------------------------- /dev-requirements.txt: -------------------------------------------------------------------------------- 1 | --editable . 2 | nose 3 | mock 4 | coverage 5 | nose-cov 6 | sphinx 7 | pyandoc 8 | tox 9 | -------------------------------------------------------------------------------- /docs/source/usage_library.rst: -------------------------------------------------------------------------------- 1 | Using the wpa_supplicant Library 2 | ================================ 3 | 4 | TODO 5 | -------------------------------------------------------------------------------- /run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | nosetests --config=etc/nose.cfg --with-cov --cov=wpa_supplicant --cov-config=etc/coverage.cfg --cov-report=html 4 | -------------------------------------------------------------------------------- /wpa_supplicant/test/test_cli.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class TestCLI(unittest.TestCase): 5 | def test_cli_import(self): 6 | from wpa_supplicant import cli 7 | -------------------------------------------------------------------------------- /wpa_supplicant/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015 Digi International Inc. All Rights Reserved. 6 | -------------------------------------------------------------------------------- /wpa_supplicant/test/__init__.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015 Digi International Inc. All Rights Reserved. 6 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py33,py34,pypy 3 | 4 | [testenv] 5 | passenv = TRAVIS TRAVIS_JOB_ID TRAVIS_BRANCH 6 | deps= 7 | -rdev-requirements.txt 8 | commands=nosetests 9 | 10 | [testenv:coverage] 11 | deps= 12 | {[testenv]deps} 13 | python-coveralls 14 | commands = 15 | coverage run --branch --omit={envdir}/* {envbindir}/nosetests 16 | coveralls 17 | -------------------------------------------------------------------------------- /etc/coverage.cfg: -------------------------------------------------------------------------------- 1 | [report] 2 | omit = *cli* 3 | exclude_lines = 4 | # Have to re-enable the standard pragma 5 | pragma: no cover 6 | 7 | # Don't complain about missing debug-only code: 8 | def __repr__ 9 | def __str__ 10 | if self\.debug 11 | 12 | # Don't complain if tests don't hit defensive assertion code: 13 | raise AssertionError 14 | raise NotImplementedError 15 | 16 | # Don't complain if non-runnable code isn't run: 17 | if 0: 18 | if __name__ == .__main__.: 19 | 20 | [html] 21 | directory = cover 22 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | # this version of python is only used to run tox - the version specified by TOX_ENV 4 | # is used to install and run tests 5 | python: 2.7 6 | env: 7 | - TOX_ENV=py27 8 | - TOX_ENV=py33 9 | - TOX_ENV=py34 10 | - TOX_ENV=pypy 11 | - TOX_ENV=coverage 12 | 13 | # command to install dependencies, e.g. pip install -r requirements.txt --use-mirrors 14 | install: 15 | - pip install tox 16 | - pip install -r dev-requirements.txt 17 | 18 | # command to run tests, e.g. python setup.py test 19 | script: 20 | - tox -e $TOX_ENV 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## Python wpa_supplicant Library Changelog 2 | 3 | ### 0.2 / 2015-06-03 4 | [Full Changelog](https://github.com/digidotcom/python-wpa-supplicant/0.1...0.2) 5 | 6 | Enhancements: 7 | 8 | * Python 3.3+ Support added in addition to Python 2.7 support. 9 | * Minor documentation updates 10 | * Better continuous integration and testing support in the library. 11 | 12 | ### 0.1 / 2015-06-03 13 | 14 | Initial Release of the library with a large chunk of initial 15 | functionality based on real project work. Documentation present but 16 | somewhat limited and only supports Python 2.7. 17 | -------------------------------------------------------------------------------- /docs/source/api.rst: -------------------------------------------------------------------------------- 1 | API Documentation 2 | ================= 3 | 4 | WpaSupplicantDriver 5 | ------------------- 6 | 7 | .. autoclass:: wpa_supplicant.core.WpaSupplicantDriver 8 | :members: 9 | 10 | WpaSupplicant 11 | ------------- 12 | 13 | .. autoclass:: wpa_supplicant.core.WpaSupplicant 14 | :members: 15 | 16 | Interface 17 | --------- 18 | 19 | .. autoclass:: wpa_supplicant.core.Interface 20 | :members: 21 | 22 | Network 23 | ------- 24 | 25 | .. autoclass:: wpa_supplicant.core.Network 26 | :members: 27 | 28 | BSS 29 | --- 30 | 31 | .. autoclass:: wpa_supplicant.core.BSS 32 | :members: -------------------------------------------------------------------------------- /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | # 2 | # ---Choose Your Languages--- 3 | # To disable analysis for a certain language, set the language to `false`. 4 | # For help setting your languages: 5 | # http://docs.codeclimate.com/article/169-configuring-analysis-languages 6 | # 7 | languages: 8 | Ruby: false 9 | Javascript: false 10 | PHP: false 11 | Python: true 12 | 13 | # 14 | # ---Exclude Files or Directories--- 15 | # List the files or directories you would like excluded from analysis. 16 | # For help setting your exclude paths: 17 | # http://docs.codeclimate.com/article/166-excluding-files-folders 18 | # 19 | exclude_paths: 20 | - wpa_supplicant/test/** 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | env2/ 13 | env3/ 14 | .toxenv/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | *.egg-info/ 27 | .installed.cfg 28 | *.egg 29 | README.rst 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *,cover 50 | cover/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | 59 | # Sphinx documentation 60 | docs/_build/ 61 | 62 | # PyBuilder 63 | target/ 64 | 65 | # PyCharm 66 | .idea/ -------------------------------------------------------------------------------- /HACKING.md: -------------------------------------------------------------------------------- 1 | Developer's Guide 2 | ================= 3 | 4 | Welcome to the developer's guide for the Python wpa_supplicant library. 5 | 6 | Setup Development Environment 7 | ----------------------------- 8 | 9 | Use `virtualenv` to create a Python environment. 10 | 11 | ```sh 12 | $ virtualenv -p /usr/bin/python2 env2 13 | ``` 14 | 15 | Activate one of the environments. 16 | 17 | ```sh 18 | $ source env2/bin/activate 19 | ``` 20 | 21 | Then, install the development dependencies. 22 | 23 | ```sh 24 | (env2) $ pip install -r dev-requirements.txt 25 | ``` 26 | 27 | 28 | Running tests 29 | ------------- 30 | 31 | This project uses `nose` for finding and executing tests and `coverage` for generating 32 | HTML based coverage reports. 33 | 34 | Running the tests can be as easy as: 35 | 36 | ```sh 37 | $ nosetests 38 | ``` 39 | 40 | However, running the tests is highly configurable and for this project it is easiest 41 | to run a script which makes use of config files under `etc/`. 42 | 43 | ```sh 44 | $ ./run_tests.sh 45 | ``` 46 | 47 | Check out the coverage reports: 48 | 49 | ```sh 50 | $ firefox cover/index.html 51 | ``` 52 | -------------------------------------------------------------------------------- /wpa_supplicant/examples/example_scan.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015 Digi International Inc. All Rights Reserved. 6 | 7 | from wpa_supplicant.core import WpaSupplicantDriver 8 | from twisted.internet.selectreactor import SelectReactor 9 | import threading 10 | import time 11 | 12 | # Start a simple Twisted SelectReactor 13 | reactor = SelectReactor() 14 | threading.Thread(target=reactor.run, kwargs={'installSignalHandlers': 0}).start() 15 | time.sleep(0.1) # let reactor start 16 | 17 | # Start Driver 18 | driver = WpaSupplicantDriver(reactor) 19 | 20 | # Connect to the supplicant, which returns the "root" D-Bus object for wpa_supplicant 21 | supplicant = driver.connect() 22 | 23 | # Register an interface w/ the supplicant, this can raise an error if the supplicant 24 | # already knows about this interface 25 | interface = supplicant.create_interface('wlan0') 26 | 27 | # Issue the scan 28 | scan_results = interface.scan(block=True) 29 | for bss in scan_results: 30 | print bss.get_ssid() 31 | -------------------------------------------------------------------------------- /docs/source/tips_and_tricks.rst: -------------------------------------------------------------------------------- 1 | Development Tips and Tricks 2 | =========================== 3 | 4 | D-Bus Configuration 5 | ------------------- 6 | 7 | Give your user permissions to use the D-Bus system bus: 8 | 9 | Add the following lines to the following files: 10 | 11 | **/etc/dbus-1/system.d/org.freedesktop.ModemManager1.conf** 12 | 13 | .. code-block:: shell 14 | 15 | 16 | 17 | 18 | 19 | 20 | **/etc/dbus-1/system.d/wpa_supplicant.conf** 21 | 22 | .. code-block:: shell 23 | 24 | 25 | 26 | 27 | 28 | 29 | Then, reset dbus: 30 | 31 | .. code-block:: shell 32 | 33 | $ /etc/init.d/dbus stop 34 | $ /etc/init.d/dbus start 35 | 36 | 37 | Disabling NetworkManager on Ubuntu 38 | ---------------------------------- 39 | 40 | It is often desired to disable NetworkMananger on your dev box because the WDNU-II 41 | application is essentially the same thing and they likely do not play nicely together. 42 | To do so while keeping your DHCP ethernet connection intact (interwebs), do the following: 43 | 44 | .. code-block:: shell 45 | 46 | $ sudo service network-manager stop 47 | $ sudo ifconfig eth0 up (make sure your interface is actually eth0!!) 48 | $ sudo dhclient eth0 49 | -------------------------------------------------------------------------------- /feature_map.csv: -------------------------------------------------------------------------------- 1 | Object,Method,Supported 2 | root,CreateInterface,X 3 | ,RemoveInterface,X 4 | ,GetInterface,X 5 | ,Get (properties),X 6 | ,Set (properties),X 7 | ,Register (signal),X 8 | Interface,Scan,X 9 | ,Disconnect,X 10 | ,AddNetwork,X 11 | ,RemoveNetwork,X 12 | ,RemoveAllNetworks,X 13 | ,SelectNetwork,X 14 | ,Reassociate, 15 | ,Reattach, 16 | ,AddBlob, 17 | ,RemoveBlob, 18 | ,GetBlob, 19 | ,AutoScan, 20 | ,TDLSDiscover, 21 | ,TDLSSetup, 22 | ,TDLSStatus, 23 | ,TDLSTeardown, 24 | ,EAPLogoff, 25 | ,EAPLogon, 26 | ,NetworkReply, 27 | ,SetPKCS11EngineAndModulePath, 28 | ,SignalPoll, 29 | ,FlushBSS, 30 | ,SubscribeProbReq, 31 | ,UnsubscribeProbReq, 32 | ,Get (properties),X 33 | ,Set (properties),X 34 | ,Register (signal),X 35 | Interface.WPS,Start, 36 | ,Get (properties), 37 | ,Set (properties), 38 | ,Register (signal), 39 | Interface.P2PDevice,Find, 40 | ,StopFind, 41 | ,Listen, 42 | ,ExtendedListen, 43 | ,PresenceRequest, 44 | ,ProvisionDiscoveryRequest, 45 | ,Connect, 46 | ,GroupAdd, 47 | ,Invite, 48 | ,Disconnect, 49 | ,RejectPeer, 50 | ,Flush, 51 | ,AddService, 52 | ,DeleteService, 53 | ,FlushService, 54 | ,ServiceDiscoveryRequest, 55 | ,ServiceDiscoveryResponse, 56 | ,ServiceDiscoveryCancelRequest, 57 | ,ServiceUpdate, 58 | ,Register (signal), 59 | BSS,Get (properties),X 60 | ,Set (properties),X 61 | ,Register (signal),X 62 | Network,Get (properties),X 63 | ,Set (properties),X 64 | ,Register (signal),X 65 | Peer,Get (properties), 66 | ,Set (properties), 67 | ,Register (signal), 68 | Group,Get (properties), 69 | ,Set (properties), 70 | ,Register (signal), 71 | PersistentGroup,Get (properties), 72 | ,Set (properties), 73 | ,Register (signal), 74 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. python-wpa-supplicant documentation master file, created by 2 | sphinx-quickstart on Wed Jun 3 14:30:47 2015. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to python-wpa-supplicant's documentation! 7 | ================================================= 8 | 9 | This library provides an interface to the wpa_supplicant D-Bus interface. As of now, 10 | there is really no good option for interfacing with wpa_supplicant from Python and 11 | to go a step further, the existing D-Bus libraries are difficult to work with. This 12 | library abstracts all of that away into a very clean API based on the 13 | `wpa_supplicant D-Bus documentation `_ 14 | 15 | In addition to the Python library, this package also includes a wpa_supplicant CLI tool 16 | which provides access to the entire library via command line! 17 | 18 | 19 | Documentation Contents 20 | ---------------------- 21 | 22 | The main documentation seeks to provide a broad overview showing how to use 23 | the library and CLI at a high level but without going into all the minute 24 | details. 25 | 26 | .. toctree:: 27 | :maxdepth: 2 28 | 29 | usage_cli 30 | usage_library 31 | tips_and_tricks 32 | 33 | API Reference 34 | ------------- 35 | 36 | The API reference includes automatically generated documentation 37 | that covers the details of individual classes, methods, and 38 | members. 39 | 40 | .. toctree:: 41 | :maxdepth: 2 42 | 43 | api 44 | 45 | 46 | Indices and tables 47 | ================== 48 | 49 | * :ref:`genindex` 50 | * :ref:`modindex` 51 | * :ref:`search` 52 | 53 | -------------------------------------------------------------------------------- /toxtest.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Script that will try to test this codebase against as many 4 | # python versions as is possible. It does this using a combination 5 | # of pyenv (for building various interpreters) and tox for 6 | # testing using each of those interpreters. 7 | # 8 | 9 | pyversions=(2.7.7 10 | 3.3.5 11 | 3.4.3 12 | pypy-2.3.1) 13 | 14 | # parse options 15 | for i in "$@" 16 | do 17 | case $i in 18 | --fast) 19 | FAST=YES 20 | ;; 21 | esac 22 | done 23 | 24 | if [ ! FAST="YES" ]; then 25 | # first make sure that pyenv is installed 26 | if [ ! -s "$HOME/.pyenv/bin/pyenv" ]; then 27 | curl -L https://raw.githubusercontent.com/yyuu/pyenv-installer/master/bin/pyenv-installer | bash 28 | fi 29 | fi 30 | 31 | # Update pyenv (required for new python versions to be available) 32 | (cd $HOME/.pyenv && git pull) 33 | 34 | # add pyenv to our path and initialize (if this has not already been done) 35 | export PATH="$HOME/.pyenv/bin:$PATH" 36 | eval "$(pyenv init -)" 37 | 38 | # install each python version that we want to test with 39 | for pyversion in ${pyversions[*]}; 40 | do 41 | pyenv install -s ${pyversion} 42 | done 43 | pyenv rehash 44 | 45 | # This is required 46 | pyenv global ${pyversions[*]} 47 | 48 | # Now, run the tests after sourcing venv for tox install/use 49 | if [ "$FAST" != "YES" ]; then 50 | virtualenv -q .toxenv 51 | fi 52 | source .toxenv/bin/activate 53 | if [ "$FAST" != "YES" ]; then 54 | pip install -q -r dev-requirements.txt 55 | fi 56 | 57 | if [ "$FAST" == "YES" ]; then 58 | tox 59 | else 60 | # will ensure all depencies are pulled in 61 | tox --recreate 62 | fi 63 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015 Digi International Inc. All Rights Reserved. 6 | 7 | from setuptools import setup, find_packages 8 | import os 9 | 10 | install_requires = [ 11 | 'txdbus>=1.0.1', 12 | 'click', 13 | 'six', 14 | ] 15 | 16 | 17 | def get_long_description(): 18 | long_description = open('README.md').read() 19 | try: 20 | import subprocess 21 | import pandoc 22 | 23 | process = subprocess.Popen( 24 | ['which pandoc'], 25 | shell=True, 26 | stdout=subprocess.PIPE, 27 | universal_newlines=True) 28 | 29 | pandoc_path = process.communicate()[0] 30 | pandoc_path = pandoc_path.strip('\n') 31 | 32 | pandoc.core.PANDOC_PATH = pandoc_path 33 | 34 | doc = pandoc.Document() 35 | doc.markdown = long_description 36 | long_description = doc.rst 37 | open("README.rst", "w").write(doc.rst) 38 | except: 39 | if os.path.exists("README.rst"): 40 | long_description = open("README.rst").read() 41 | else: 42 | print("Could not find pandoc or convert properly") 43 | print(" make sure you have pandoc (system) and pyandoc (python module) installed") 44 | 45 | return long_description 46 | 47 | 48 | setup( 49 | name='wpa_supplicant', 50 | version='0.2', 51 | description='WPA Supplicant wrapper for Python', 52 | long_description=get_long_description(), 53 | author="Stephen Stack", 54 | author_email="Stephen.Stack@digi.com", 55 | install_requires=install_requires, 56 | packages=find_packages(), 57 | entry_points={ 58 | 'console_scripts': ['wpa=wpa_supplicant.cli:run'] 59 | }, 60 | classifiers=[ 61 | "Development Status :: 3 - Alpha", 62 | "Intended Audience :: Developers", 63 | "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)", 64 | "Programming Language :: Python :: 2.7", 65 | "Topic :: Software Development :: Libraries", 66 | "Operating System :: POSIX :: Linux", 67 | ], 68 | ) 69 | -------------------------------------------------------------------------------- /wpa_supplicant/examples/example_txdbus.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015 Digi International Inc. All Rights Reserved. 6 | 7 | """Example of using the raw txdbus APIs 8 | 9 | Note: There are some hardcoded values in this example and should not be expected 10 | to run on every PC but serves as a reference. 11 | """ 12 | 13 | from twisted.internet import reactor, defer 14 | from txdbus import client, error 15 | from txdbus.interface import DBusInterface, Method, Signal 16 | import time 17 | 18 | dbus_name = 'fi.w1.wpa_supplicant1' 19 | root_iface_str = 'fi.w1.wpa_supplicant1' 20 | root_iface_obj_path = "/" + root_iface_str.replace('.', '/') 21 | 22 | interface_iface_str = root_iface_str + ".Interface" 23 | interface_iface = DBusInterface(interface_iface_str, 24 | Signal('ScanDone', 'b'), 25 | Method('Scan', arguments='a{sv}'), 26 | Method('AddNetwork', arguments='a{sv}', returns='o'), 27 | Method('RemoveNetwork', arguments='o'), 28 | Method('SelectNetwork', arguments='o')) 29 | 30 | 31 | @defer.inlineCallbacks 32 | def set_mode_example(): 33 | cli = yield client.connect(reactor, busAddress='system') 34 | 35 | root_obj = yield cli.getRemoteObject(dbus_name, root_iface_obj_path) 36 | print "Root Object: %s" % root_obj 37 | 38 | wlan0_obj_path = yield root_obj.callRemote('GetInterface', 'wlan1') 39 | print "WLAN0 Object Path: %s" % wlan0_obj_path 40 | 41 | wlan0_obj = yield cli.getRemoteObject(dbus_name, wlan0_obj_path, interface_iface) 42 | print "WLAN0 Object: %s" % wlan0_obj 43 | 44 | # wpa_supplicant will validate keys, but does not validate values. 45 | # There are a bunch of additional configuration options for 801.11ac. 46 | network_settings = { 47 | "disabled": 0, # Not disabled 48 | # "id_str": "ExtScriptID", 49 | "ssid": "funny-ssid", # SSID 50 | # "frequency": 2412, # When IBSS only, ignored in mode=0 51 | # "proto": "WPA", 52 | # "bssid": "Somebss", 53 | "mode": 0, # 0 = managed(default), 1 = IBSS (Ad-hoc, p2p), 2: AP (access point) 54 | # "freq_list": None, # 55 | # "bgscan": "simple:30:-45:300", 56 | # "proto": "WPA", 57 | "key_mgmt": "WPA-PSK", 58 | # "ieee80211w": 0, 59 | "auth_alg": "OPEN", 60 | "pairwise": "CCMP", 61 | # "group": None, 62 | "psk": "funnypsk", 63 | # "eapol_flags": 3, 64 | # "macsec_policy": 0, 65 | # "mixed_cell": 0, 66 | # "proactive_key_caching": 0, 67 | # "wep_key0": None, 68 | # "wep_key1": None, 69 | # "wep_key2": None, 70 | # "wep_key3": None, 71 | # "wep_tx_keyidx": None, 72 | # "peerkey": 0, 73 | # "wpa_ptk_rekey": 100000, 74 | # "eap": "MD5 MSCHAPV2 OTP GTC TLS PEAP TTLS", 75 | # "identity": "Some one", # EAP identity 76 | # "anonymous_identity": "Anonymous one", # EAP anonymous 77 | # "password": "Some one's password", 78 | # "ca_cert": "file path to CA certificates", 79 | # "ca_path": "directory path for CA certificate files (PEM)", 80 | # "client_cert": "file path to client certificate file", 81 | # "private_key": "file path to private key", 82 | # "private_key_passwd": "password for private key", 83 | # "dh_file": "file path to DH/DSA params", 84 | # "subject_match": "x", 85 | # "altsubject_match": "alt", 86 | # "phase1": "peapver=1", # See wpa_supplicant.conf for phase1 TLS details 87 | # "phase2": "auth=MSCHAPV2", # See wpa_supplicant.conf for phase2 TLS details 88 | # "ca_cert2": "file path to phase2 CA certificates", 89 | # "ca_path2": "directory path for phase2 CA certificate files (PEM)", 90 | # "client_cert2": "file path to phase2 client certificate", 91 | # "private_key2": "file path to phase2 client private key", 92 | # "private_key2_passwd": "password for phase2 private key", 93 | # "dh_file2": "file path to phase2 DH/DSA params", 94 | # "subject_match2": "x", 95 | # "altsubject_match2": "alt2", 96 | # "fragment_size": "1398", 97 | # "ocsp": 0, 98 | # "ap_max_inactivity": 300, # Seconds 99 | # "disable_ht": "0", # High Throughput 802.11n enabled 100 | # "disable_ht40": 0, # High Throughput 802.11n enabled (40 MHz channel) 101 | # "disable_sgi": 0, # short guard interval enabled 102 | # "disable_ldpc": 0, # low density parity check enabled 103 | # "ht40_intolerant": 0, # HT-40 tolerated 104 | # "ht_mcs": "", # Use all available 105 | # "disable_max_amsdu": -1, 106 | # "ampdu_factor": 3, 107 | # "ampdu_density": -1, 108 | # "disable_vht": 0, # Very High Throughput 802.11ac enabled 109 | } 110 | 111 | network_obj = yield wlan0_obj.callRemote('AddNetwork', network_settings) 112 | print "Network Object Path: %s" % network_obj 113 | yield wlan0_obj.callRemote('SelectNetwork', network_obj) 114 | time.sleep(20) 115 | yield wlan0_obj.callRemote('RemoveNetwork', network_obj) 116 | reactor.stop() 117 | 118 | 119 | reactor.callWhenRunning(set_mode_example) 120 | reactor.run() 121 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = build 9 | 10 | # User-friendly check for sphinx-build 11 | ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) 12 | $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) 13 | endif 14 | 15 | # Internal variables. 16 | PAPEROPT_a4 = -D latex_paper_size=a4 17 | PAPEROPT_letter = -D latex_paper_size=letter 18 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 19 | # the i18n builder cannot share the environment and doctrees with the others 20 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source 21 | 22 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext 23 | 24 | help: 25 | @echo "Please use \`make ' where is one of" 26 | @echo " html to make standalone HTML files" 27 | @echo " dirhtml to make HTML files named index.html in directories" 28 | @echo " singlehtml to make a single large HTML file" 29 | @echo " pickle to make pickle files" 30 | @echo " json to make JSON files" 31 | @echo " htmlhelp to make HTML files and a HTML help project" 32 | @echo " qthelp to make HTML files and a qthelp project" 33 | @echo " applehelp to make an Apple Help Book" 34 | @echo " devhelp to make HTML files and a Devhelp project" 35 | @echo " epub to make an epub" 36 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 37 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 38 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" 39 | @echo " text to make text files" 40 | @echo " man to make manual pages" 41 | @echo " texinfo to make Texinfo files" 42 | @echo " info to make Texinfo files and run them through makeinfo" 43 | @echo " gettext to make PO message catalogs" 44 | @echo " changes to make an overview of all changed/added/deprecated items" 45 | @echo " xml to make Docutils-native XML files" 46 | @echo " pseudoxml to make pseudoxml-XML files for display purposes" 47 | @echo " linkcheck to check all external links for integrity" 48 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 49 | @echo " coverage to run coverage check of the documentation (if enabled)" 50 | 51 | clean: 52 | rm -rf $(BUILDDIR)/* 53 | 54 | html: 55 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 56 | @echo 57 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 58 | 59 | dirhtml: 60 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 61 | @echo 62 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 63 | 64 | singlehtml: 65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 66 | @echo 67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 68 | 69 | pickle: 70 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 71 | @echo 72 | @echo "Build finished; now you can process the pickle files." 73 | 74 | json: 75 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 76 | @echo 77 | @echo "Build finished; now you can process the JSON files." 78 | 79 | htmlhelp: 80 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 81 | @echo 82 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 83 | ".hhp project file in $(BUILDDIR)/htmlhelp." 84 | 85 | qthelp: 86 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 87 | @echo 88 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 89 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 90 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/python-wpa-supplicant.qhcp" 91 | @echo "To view the help file:" 92 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-wpa-supplicant.qhc" 93 | 94 | applehelp: 95 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp 96 | @echo 97 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." 98 | @echo "N.B. You won't be able to view it unless you put it in" \ 99 | "~/Library/Documentation/Help or install it in your application" \ 100 | "bundle." 101 | 102 | devhelp: 103 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 104 | @echo 105 | @echo "Build finished." 106 | @echo "To view the help file:" 107 | @echo "# mkdir -p $$HOME/.local/share/devhelp/python-wpa-supplicant" 108 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/python-wpa-supplicant" 109 | @echo "# devhelp" 110 | 111 | epub: 112 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 113 | @echo 114 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 115 | 116 | latex: 117 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 118 | @echo 119 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 120 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 121 | "(use \`make latexpdf' here to do that automatically)." 122 | 123 | latexpdf: 124 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 125 | @echo "Running LaTeX files through pdflatex..." 126 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 127 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 128 | 129 | latexpdfja: 130 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 131 | @echo "Running LaTeX files through platex and dvipdfmx..." 132 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja 133 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 134 | 135 | text: 136 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 137 | @echo 138 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 139 | 140 | man: 141 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 142 | @echo 143 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 144 | 145 | texinfo: 146 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 147 | @echo 148 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 149 | @echo "Run \`make' in that directory to run these through makeinfo" \ 150 | "(use \`make info' here to do that automatically)." 151 | 152 | info: 153 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 154 | @echo "Running Texinfo files through makeinfo..." 155 | make -C $(BUILDDIR)/texinfo info 156 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 157 | 158 | gettext: 159 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 160 | @echo 161 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 162 | 163 | changes: 164 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 165 | @echo 166 | @echo "The overview file is in $(BUILDDIR)/changes." 167 | 168 | linkcheck: 169 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 170 | @echo 171 | @echo "Link check complete; look for any errors in the above output " \ 172 | "or in $(BUILDDIR)/linkcheck/output.txt." 173 | 174 | doctest: 175 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 176 | @echo "Testing of doctests in the sources finished, look at the " \ 177 | "results in $(BUILDDIR)/doctest/output.txt." 178 | 179 | coverage: 180 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage 181 | @echo "Testing of coverage in the sources finished, look at the " \ 182 | "results in $(BUILDDIR)/coverage/python.txt." 183 | 184 | xml: 185 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml 186 | @echo 187 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml." 188 | 189 | pseudoxml: 190 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml 191 | @echo 192 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." 193 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source 10 | set I18NSPHINXOPTS=%SPHINXOPTS% source 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. xml to make Docutils-native XML files 37 | echo. pseudoxml to make pseudoxml-XML files for display purposes 38 | echo. linkcheck to check all external links for integrity 39 | echo. doctest to run all doctests embedded in the documentation if enabled 40 | echo. coverage to run coverage check of the documentation if enabled 41 | goto end 42 | ) 43 | 44 | if "%1" == "clean" ( 45 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 46 | del /q /s %BUILDDIR%\* 47 | goto end 48 | ) 49 | 50 | 51 | REM Check if sphinx-build is available and fallback to Python version if any 52 | %SPHINXBUILD% 2> nul 53 | if errorlevel 9009 goto sphinx_python 54 | goto sphinx_ok 55 | 56 | :sphinx_python 57 | 58 | set SPHINXBUILD=python -m sphinx.__init__ 59 | %SPHINXBUILD% 2> nul 60 | if errorlevel 9009 ( 61 | echo. 62 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 63 | echo.installed, then set the SPHINXBUILD environment variable to point 64 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 65 | echo.may add the Sphinx directory to PATH. 66 | echo. 67 | echo.If you don't have Sphinx installed, grab it from 68 | echo.http://sphinx-doc.org/ 69 | exit /b 1 70 | ) 71 | 72 | :sphinx_ok 73 | 74 | 75 | if "%1" == "html" ( 76 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 77 | if errorlevel 1 exit /b 1 78 | echo. 79 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 80 | goto end 81 | ) 82 | 83 | if "%1" == "dirhtml" ( 84 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 85 | if errorlevel 1 exit /b 1 86 | echo. 87 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 88 | goto end 89 | ) 90 | 91 | if "%1" == "singlehtml" ( 92 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 93 | if errorlevel 1 exit /b 1 94 | echo. 95 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 96 | goto end 97 | ) 98 | 99 | if "%1" == "pickle" ( 100 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 101 | if errorlevel 1 exit /b 1 102 | echo. 103 | echo.Build finished; now you can process the pickle files. 104 | goto end 105 | ) 106 | 107 | if "%1" == "json" ( 108 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 109 | if errorlevel 1 exit /b 1 110 | echo. 111 | echo.Build finished; now you can process the JSON files. 112 | goto end 113 | ) 114 | 115 | if "%1" == "htmlhelp" ( 116 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 117 | if errorlevel 1 exit /b 1 118 | echo. 119 | echo.Build finished; now you can run HTML Help Workshop with the ^ 120 | .hhp project file in %BUILDDIR%/htmlhelp. 121 | goto end 122 | ) 123 | 124 | if "%1" == "qthelp" ( 125 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 129 | .qhcp project file in %BUILDDIR%/qthelp, like this: 130 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\python-wpa-supplicant.qhcp 131 | echo.To view the help file: 132 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\python-wpa-supplicant.ghc 133 | goto end 134 | ) 135 | 136 | if "%1" == "devhelp" ( 137 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 138 | if errorlevel 1 exit /b 1 139 | echo. 140 | echo.Build finished. 141 | goto end 142 | ) 143 | 144 | if "%1" == "epub" ( 145 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 146 | if errorlevel 1 exit /b 1 147 | echo. 148 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 149 | goto end 150 | ) 151 | 152 | if "%1" == "latex" ( 153 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 154 | if errorlevel 1 exit /b 1 155 | echo. 156 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 157 | goto end 158 | ) 159 | 160 | if "%1" == "latexpdf" ( 161 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 162 | cd %BUILDDIR%/latex 163 | make all-pdf 164 | cd %~dp0 165 | echo. 166 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 167 | goto end 168 | ) 169 | 170 | if "%1" == "latexpdfja" ( 171 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 172 | cd %BUILDDIR%/latex 173 | make all-pdf-ja 174 | cd %~dp0 175 | echo. 176 | echo.Build finished; the PDF files are in %BUILDDIR%/latex. 177 | goto end 178 | ) 179 | 180 | if "%1" == "text" ( 181 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 182 | if errorlevel 1 exit /b 1 183 | echo. 184 | echo.Build finished. The text files are in %BUILDDIR%/text. 185 | goto end 186 | ) 187 | 188 | if "%1" == "man" ( 189 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 190 | if errorlevel 1 exit /b 1 191 | echo. 192 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 193 | goto end 194 | ) 195 | 196 | if "%1" == "texinfo" ( 197 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 198 | if errorlevel 1 exit /b 1 199 | echo. 200 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 201 | goto end 202 | ) 203 | 204 | if "%1" == "gettext" ( 205 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 206 | if errorlevel 1 exit /b 1 207 | echo. 208 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 209 | goto end 210 | ) 211 | 212 | if "%1" == "changes" ( 213 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 214 | if errorlevel 1 exit /b 1 215 | echo. 216 | echo.The overview file is in %BUILDDIR%/changes. 217 | goto end 218 | ) 219 | 220 | if "%1" == "linkcheck" ( 221 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 222 | if errorlevel 1 exit /b 1 223 | echo. 224 | echo.Link check complete; look for any errors in the above output ^ 225 | or in %BUILDDIR%/linkcheck/output.txt. 226 | goto end 227 | ) 228 | 229 | if "%1" == "doctest" ( 230 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 231 | if errorlevel 1 exit /b 1 232 | echo. 233 | echo.Testing of doctests in the sources finished, look at the ^ 234 | results in %BUILDDIR%/doctest/output.txt. 235 | goto end 236 | ) 237 | 238 | if "%1" == "coverage" ( 239 | %SPHINXBUILD% -b coverage %ALLSPHINXOPTS% %BUILDDIR%/coverage 240 | if errorlevel 1 exit /b 1 241 | echo. 242 | echo.Testing of coverage in the sources finished, look at the ^ 243 | results in %BUILDDIR%/coverage/python.txt. 244 | goto end 245 | ) 246 | 247 | if "%1" == "xml" ( 248 | %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml 249 | if errorlevel 1 exit /b 1 250 | echo. 251 | echo.Build finished. The XML files are in %BUILDDIR%/xml. 252 | goto end 253 | ) 254 | 255 | if "%1" == "pseudoxml" ( 256 | %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml 257 | if errorlevel 1 exit /b 1 258 | echo. 259 | echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. 260 | goto end 261 | ) 262 | 263 | :end 264 | -------------------------------------------------------------------------------- /wpa_supplicant/cli.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015 Digi International Inc. All Rights Reserved. 6 | 7 | # TODO: There is no good way to accept array type arguments 8 | 9 | import click 10 | from contextlib import contextmanager 11 | import threading 12 | from twisted.internet.selectreactor import SelectReactor 13 | import time 14 | from wpa_supplicant.core import WpaSupplicantDriver, BSS 15 | import pprint 16 | import sys 17 | 18 | 19 | # 20 | # Helpers 21 | # 22 | @contextmanager 23 | def supplicant(): 24 | """Run a reactor and provide access to the supplicant driver""" 25 | reactor = SelectReactor() 26 | t = threading.Thread(target=reactor.run, kwargs={'installSignalHandlers': 0}) 27 | t.start() 28 | time.sleep(0.1) # let reactor start 29 | driver = WpaSupplicantDriver(reactor) 30 | supplicant = driver.connect() 31 | try: 32 | yield supplicant 33 | except Exception as e: 34 | print('FAIL - {}'.format(e)) 35 | else: 36 | print('OK') 37 | reactor.disconnectAll() 38 | reactor.sigTerm() 39 | t.join() 40 | 41 | 42 | def add_optional_args(args, *optionals): 43 | for opt in optionals: 44 | if opt is not None: 45 | args.append(opt) 46 | return args 47 | 48 | 49 | def ctx_get(ctx, key, default=None): 50 | """Iterate up context tree and return the value at `key` if found""" 51 | 52 | while not getattr(ctx, 'is_root', False): 53 | value = getattr(ctx, key, None) 54 | if value is not None: 55 | return value 56 | ctx = ctx.parent 57 | return default 58 | 59 | 60 | # 61 | # The following CLI "groups" follow the natural hierarchy of the D-Bus 62 | # object system: 63 | # 64 | # * fi.w1.wpa_supplicant1 65 | # * fi.w1.wpa_supplicant1.Interface 66 | # * fi.w1.wpa_supplicant1.Interface.WPS 67 | # * fi.w1.wpa_supplicant1.Interface.P2PDevice 68 | # * fi.w1.wpa_supplicant1.BSS 69 | # * fi.w1.wpa_supplicant1.Network 70 | # * fi.w1.wpa_supplicant1.Peer 71 | # * fi.w1.wpa_supplicant1.Group 72 | # * fi.w1.wpa_supplicant1.PersistentGroup 73 | # 74 | 75 | @click.group() 76 | @click.option('--debug/--no-debug', default=False, help="Show log debug on stdout") 77 | @click.pass_context 78 | def root(ctx, debug): 79 | """Command line interface for wpa_supplicant D-Bus""" 80 | ctx.is_root = True 81 | 82 | 83 | @root.group() 84 | @click.argument('ifname', 'e.g. wlan0') 85 | @click.pass_context 86 | def interface(ctx, ifname): 87 | """Access fi.w1.wpa_supplicant1.Interface object""" 88 | ctx.ifname = ifname 89 | 90 | 91 | @interface.group(name='wps') 92 | def interface_wps(): 93 | """Access fi.w1.wpa_supplicant1.Interface.WPS object""" 94 | raise NotImplemented 95 | 96 | 97 | @interface.group(name='p2p_device') 98 | def interface_p2p_device(): 99 | """Access fi.w1.wpa_supplicant1.Interface.P2PDevice object""" 100 | raise NotImplemented 101 | 102 | 103 | @root.group() 104 | @click.argument('ifname', 'e.g. wlan0') 105 | @click.option('--ssid', default=None, help='Look at scan results for BSS examples') 106 | @click.option('--bssid', default=None, help='Look at scan results for BSS examples') 107 | @click.pass_context 108 | def bss(ctx, ifname, ssid, bssid): 109 | """Access fi.w1.wpa_supplicant1.BSS object""" 110 | ctx.ifname = ifname 111 | ctx.ssid = None 112 | ctx.bssid = None 113 | at_least_one_option = False 114 | if ssid is not None: 115 | ctx.ssid = ssid 116 | at_least_one_option = True 117 | if bssid is not None: 118 | ctx.bssid = bssid 119 | at_least_one_option = True 120 | if not at_least_one_option: 121 | print('BSS sub-commands require a valid ssid or bssid option') 122 | sys.exit(1) 123 | 124 | 125 | @root.group() 126 | def network(): 127 | """Access fi.w1.wpa_supplicant1.Network object""" 128 | 129 | 130 | @root.group() 131 | def peer(): 132 | """Access fi.w1.wpa_supplicant1.Peer object""" 133 | raise NotImplemented 134 | 135 | 136 | @root.group() 137 | def group(): 138 | """Access fi.w1.wpa_supplicant1.Group object""" 139 | raise NotImplemented 140 | 141 | 142 | @root.group() 143 | def persistent_group(): 144 | """Access fi.w1.wpa_supplicant1.PersistentGroup object""" 145 | raise NotImplemented 146 | 147 | 148 | # 149 | # fi.w1.wpa_supplicant1 API 150 | # 151 | @root.command() 152 | @click.argument('ifname', 'e.g. wlan0') 153 | @click.option('--bridge_if_name', default=None, help='Bridge to control, e.g., br0') 154 | @click.option('--driver', default=None, help='e.g. nl80211') 155 | @click.option('--config_file', default=None, help='Config file path') 156 | def create_interface(ifname, bridge_if_name, driver, config_file): 157 | """Method: Registers a wireless interface in wpa_supplicant""" 158 | args = add_optional_args([ifname, ], bridge_if_name, driver, config_file) 159 | with supplicant() as supp: 160 | pprint.pprint(supp.create_interface(*args)) 161 | 162 | 163 | @root.command() 164 | @click.argument('ifname', 'e.g. wlan0') 165 | def remove_interface(ifname): 166 | """Method: Deregisters a wireless interface from wpa_supplicant""" 167 | with supplicant() as supp: 168 | iface = supp.get_interface(ifname) 169 | supp.remove_interface(iface.get_path()) 170 | 171 | 172 | @root.command() 173 | @click.argument('ifname', 'e.g. wlan0') 174 | def get_interface(ifname): 175 | """Method: Returns a D-Bus path to an object related to an interface which wpa_supplicant already controls""" 176 | with supplicant() as supp: 177 | pprint.pprint(supp.get_interface(ifname)) 178 | 179 | 180 | @root.command(name='get') 181 | @click.argument('name', 'Name of property (case sensitive)') 182 | def root_get(name): 183 | """Method: Get Property (case sensitive)""" 184 | with supplicant() as supp: 185 | pprint.pprint(supp.get(name)) 186 | 187 | 188 | @root.command(name='set') 189 | @click.argument('name', 'Name of property (case sensitive)') 190 | @click.argument('value', 'Value to be set') 191 | def root_set(name, value): 192 | """Method: Set Property (case sensitive)""" 193 | with supplicant() as supp: 194 | pprint.pprint(supp.set(name, value)) 195 | 196 | 197 | # 198 | # fi.w1.wpa_supplicant1.Interface API 199 | # 200 | @interface.command() 201 | @click.option('--scan_type', default='active', help='Active or Passive') 202 | @click.pass_context 203 | def scan(ctx, scan_type): 204 | """Method: Trigger a scan and block for results""" 205 | with supplicant() as supp: 206 | iface = supp.get_interface(ctx_get(ctx, 'ifname')) 207 | pprint.pprint(iface.scan(type=scan_type, block=True)) 208 | 209 | 210 | @interface.command() 211 | @click.pass_context 212 | def disconnect(ctx): 213 | """Method: Disassociates the interface from current network""" 214 | with supplicant() as supp: 215 | iface = supp.get_interface(ctx_get(ctx, 'ifname')) 216 | iface.disconnect() 217 | 218 | 219 | @interface.command(name='get') 220 | @click.argument('name', 'Name of property (case sensitive)') 221 | @click.pass_context 222 | def interface_get(ctx, name): 223 | """Method: Get Property (case sensitive)""" 224 | with supplicant() as supp: 225 | iface = supp.get_interface(ctx_get(ctx, 'ifname')) 226 | pprint.pprint(iface.get(name)) 227 | 228 | 229 | @interface.command(name='set') 230 | @click.argument('name', 'Name of property (case sensitive)') 231 | @click.argument('value', 'Value to be set') 232 | @click.pass_context 233 | def interface_set(ctx, name, value): 234 | """Method: Set Property (case sensitive)""" 235 | with supplicant() as supp: 236 | iface = supp.get_interface(ctx_get(ctx, 'ifname')) 237 | pprint.pprint(iface.set(name, value)) 238 | 239 | 240 | # 241 | # fi.w1.wpa_supplicant1.BSS API 242 | # 243 | @bss.command(name='get') 244 | @click.argument('name', 'Name of property (case sensitive)') 245 | @click.pass_context 246 | def bss_get(ctx, name): 247 | """Method: Get Property (case sensitive)""" 248 | with supplicant() as supp: 249 | iface = supp.get_interface(ctx_get(ctx, 'ifname')) 250 | scan_results = iface.scan(block=True) 251 | for result in scan_results: 252 | if result.get_ssid() == ctx_get(ctx, 'ssid') or \ 253 | result.get_bssid() == ctx_get(ctx, 'bssid'): 254 | bss = result 255 | break 256 | else: 257 | print('No BSS found') 258 | sys.exit(1) 259 | 260 | pprint.pprint(bss.get(name)) 261 | 262 | 263 | @bss.command(name='set') 264 | @click.argument('name', 'Name of property (case sensitive)') 265 | @click.argument('value', 'Value to be set') 266 | @click.pass_context 267 | def bss_set(ctx, name, value): 268 | """Method: Set Property (case sensitive)""" 269 | raise NotImplemented 270 | 271 | 272 | def run(): 273 | root() 274 | 275 | 276 | if __name__ == '__main__': 277 | run() -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # python-wpa-supplicant documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Jun 3 14:30:47 2015. 5 | # 6 | # This file is execfile()d with the current directory set to its 7 | # containing dir. 8 | # 9 | # Note that not all possible configuration values are present in this 10 | # autogenerated file. 11 | # 12 | # All configuration values have a default; values that are commented out 13 | # serve to show the default. 14 | 15 | import sys 16 | import os 17 | import shlex 18 | 19 | # If extensions (or modules to document with autodoc) are in another directory, 20 | # add these directories to sys.path here. If the directory is relative to the 21 | # documentation root, use os.path.abspath to make it absolute, like shown here. 22 | #sys.path.insert(0, os.path.abspath('.')) 23 | 24 | # -- General configuration ------------------------------------------------ 25 | 26 | # If your documentation needs a minimal Sphinx version, state it here. 27 | #needs_sphinx = '1.0' 28 | 29 | # Add any Sphinx extension module names here, as strings. They can be 30 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 31 | # ones. 32 | extensions = [ 33 | 'sphinx.ext.autodoc', 34 | 'sphinx.ext.todo', 35 | 'sphinx.ext.coverage', 36 | 'sphinx.ext.pngmath', 37 | ] 38 | 39 | # Add any paths that contain templates here, relative to this directory. 40 | templates_path = ['_templates'] 41 | 42 | # The suffix(es) of source filenames. 43 | # You can specify multiple suffix as a list of string: 44 | # source_suffix = ['.rst', '.md'] 45 | source_suffix = '.rst' 46 | 47 | # The encoding of source files. 48 | #source_encoding = 'utf-8-sig' 49 | 50 | # The master toctree document. 51 | master_doc = 'index' 52 | 53 | # General information about the project. 54 | project = u'python-wpa-supplicant' 55 | copyright = u'2015, Stephen Stack' 56 | author = u'Stephen Stack' 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The short X.Y version. 63 | version = '0.1' 64 | # The full version, including alpha/beta/rc tags. 65 | release = '0.1' 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = None 73 | 74 | # There are two options for replacing |today|: either, you set today to some 75 | # non-false value, then it is used: 76 | #today = '' 77 | # Else, today_fmt is used as the format for a strftime call. 78 | #today_fmt = '%B %d, %Y' 79 | 80 | # List of patterns, relative to source directory, that match files and 81 | # directories to ignore when looking for source files. 82 | exclude_patterns = [] 83 | 84 | # The reST default role (used for this markup: `text`) to use for all 85 | # documents. 86 | #default_role = None 87 | 88 | # If true, '()' will be appended to :func: etc. cross-reference text. 89 | #add_function_parentheses = True 90 | 91 | # If true, the current module name will be prepended to all description 92 | # unit titles (such as .. function::). 93 | #add_module_names = True 94 | 95 | # If true, sectionauthor and moduleauthor directives will be shown in the 96 | # output. They are ignored by default. 97 | #show_authors = False 98 | 99 | # The name of the Pygments (syntax highlighting) style to use. 100 | pygments_style = 'sphinx' 101 | 102 | # A list of ignored prefixes for module index sorting. 103 | #modindex_common_prefix = [] 104 | 105 | # If true, keep warnings as "system message" paragraphs in the built documents. 106 | #keep_warnings = False 107 | 108 | # If true, `todo` and `todoList` produce output, else they produce nothing. 109 | todo_include_todos = True 110 | 111 | 112 | # -- Options for HTML output ---------------------------------------------- 113 | 114 | # The theme to use for HTML and HTML Help pages. See the documentation for 115 | # a list of builtin themes. 116 | html_theme = 'alabaster' 117 | 118 | # Theme options are theme-specific and customize the look and feel of a theme 119 | # further. For a list of options available for each theme, see the 120 | # documentation. 121 | #html_theme_options = {} 122 | 123 | # Add any paths that contain custom themes here, relative to this directory. 124 | #html_theme_path = [] 125 | 126 | # The name for this set of Sphinx documents. If None, it defaults to 127 | # " v documentation". 128 | #html_title = None 129 | 130 | # A shorter title for the navigation bar. Default is the same as html_title. 131 | #html_short_title = None 132 | 133 | # The name of an image file (relative to this directory) to place at the top 134 | # of the sidebar. 135 | #html_logo = None 136 | 137 | # The name of an image file (within the static path) to use as favicon of the 138 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 139 | # pixels large. 140 | #html_favicon = None 141 | 142 | # Add any paths that contain custom static files (such as style sheets) here, 143 | # relative to this directory. They are copied after the builtin static files, 144 | # so a file named "default.css" will overwrite the builtin "default.css". 145 | html_static_path = ['_static'] 146 | 147 | # Add any extra paths that contain custom files (such as robots.txt or 148 | # .htaccess) here, relative to this directory. These files are copied 149 | # directly to the root of the documentation. 150 | #html_extra_path = [] 151 | 152 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 153 | # using the given strftime format. 154 | #html_last_updated_fmt = '%b %d, %Y' 155 | 156 | # If true, SmartyPants will be used to convert quotes and dashes to 157 | # typographically correct entities. 158 | #html_use_smartypants = True 159 | 160 | # Custom sidebar templates, maps document names to template names. 161 | #html_sidebars = {} 162 | 163 | # Additional templates that should be rendered to pages, maps page names to 164 | # template names. 165 | #html_additional_pages = {} 166 | 167 | # If false, no module index is generated. 168 | #html_domain_indices = True 169 | 170 | # If false, no index is generated. 171 | #html_use_index = True 172 | 173 | # If true, the index is split into individual pages for each letter. 174 | #html_split_index = False 175 | 176 | # If true, links to the reST sources are added to the pages. 177 | #html_show_sourcelink = True 178 | 179 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 180 | #html_show_sphinx = True 181 | 182 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 183 | #html_show_copyright = True 184 | 185 | # If true, an OpenSearch description file will be output, and all pages will 186 | # contain a tag referring to it. The value of this option must be the 187 | # base URL from which the finished HTML is served. 188 | #html_use_opensearch = '' 189 | 190 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 191 | #html_file_suffix = None 192 | 193 | # Language to be used for generating the HTML full-text search index. 194 | # Sphinx supports the following languages: 195 | # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' 196 | # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' 197 | #html_search_language = 'en' 198 | 199 | # A dictionary with options for the search language support, empty by default. 200 | # Now only 'ja' uses this config value 201 | #html_search_options = {'type': 'default'} 202 | 203 | # The name of a javascript file (relative to the configuration directory) that 204 | # implements a search results scorer. If empty, the default will be used. 205 | #html_search_scorer = 'scorer.js' 206 | 207 | # Output file base name for HTML help builder. 208 | htmlhelp_basename = 'python-wpa-supplicantdoc' 209 | 210 | # -- Options for LaTeX output --------------------------------------------- 211 | 212 | latex_elements = { 213 | # The paper size ('letterpaper' or 'a4paper'). 214 | #'papersize': 'letterpaper', 215 | 216 | # The font size ('10pt', '11pt' or '12pt'). 217 | #'pointsize': '10pt', 218 | 219 | # Additional stuff for the LaTeX preamble. 220 | #'preamble': '', 221 | 222 | # Latex figure (float) alignment 223 | #'figure_align': 'htbp', 224 | } 225 | 226 | # Grouping the document tree into LaTeX files. List of tuples 227 | # (source start file, target name, title, 228 | # author, documentclass [howto, manual, or own class]). 229 | latex_documents = [ 230 | (master_doc, 'python-wpa-supplicant.tex', u'python-wpa-supplicant Documentation', 231 | u'Stephen Stack', 'manual'), 232 | ] 233 | 234 | # The name of an image file (relative to this directory) to place at the top of 235 | # the title page. 236 | #latex_logo = None 237 | 238 | # For "manual" documents, if this is true, then toplevel headings are parts, 239 | # not chapters. 240 | #latex_use_parts = False 241 | 242 | # If true, show page references after internal links. 243 | #latex_show_pagerefs = False 244 | 245 | # If true, show URL addresses after external links. 246 | #latex_show_urls = False 247 | 248 | # Documents to append as an appendix to all manuals. 249 | #latex_appendices = [] 250 | 251 | # If false, no module index is generated. 252 | #latex_domain_indices = True 253 | 254 | 255 | # -- Options for manual page output --------------------------------------- 256 | 257 | # One entry per manual page. List of tuples 258 | # (source start file, name, description, authors, manual section). 259 | man_pages = [ 260 | (master_doc, 'python-wpa-supplicant', u'python-wpa-supplicant Documentation', 261 | [author], 1) 262 | ] 263 | 264 | # If true, show URL addresses after external links. 265 | #man_show_urls = False 266 | 267 | 268 | # -- Options for Texinfo output ------------------------------------------- 269 | 270 | # Grouping the document tree into Texinfo files. List of tuples 271 | # (source start file, target name, title, author, 272 | # dir menu entry, description, category) 273 | texinfo_documents = [ 274 | (master_doc, 'python-wpa-supplicant', u'python-wpa-supplicant Documentation', 275 | author, 'python-wpa-supplicant', 'One line description of project.', 276 | 'Miscellaneous'), 277 | ] 278 | 279 | # Documents to append as an appendix to all manuals. 280 | #texinfo_appendices = [] 281 | 282 | # If false, no module index is generated. 283 | #texinfo_domain_indices = True 284 | 285 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 286 | #texinfo_show_urls = 'footnote' 287 | 288 | # If true, do not generate a @detailmenu in the "Top" node's menu. 289 | #texinfo_no_detailmenu = False 290 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Python wpa_supplicant Library 2 | ============================= 3 | 4 | [![Build Status](https://img.shields.io/travis/digidotcom/python-wpa-supplicant.svg)](https://travis-ci.org/digidotcom/python-wpa-supplicant) 5 | [![Coverage Status](https://img.shields.io/coveralls/digidotcom/python-wpa-supplicant.svg)](https://coveralls.io/r/digidotcom/python-wpa-supplicant) 6 | [![Code Climate](https://img.shields.io/codeclimate/github/digidotcom/python-wpa-supplicant.svg)](https://codeclimate.com/github/digidotcom/python-wpa-supplicant) 7 | [![Latest Version](https://img.shields.io/pypi/v/wpa_supplicant.svg)](https://pypi.python.org/pypi/wpa_supplicant/) 8 | [![License](https://img.shields.io/badge/license-MPL%202.0-blue.svg)](https://github.com/digidotcom/python-wpa-supplicant/blob/master/LICENSE) 9 | 10 | * [Full Documentation](http://digidotcom.github.io/python-wpa-supplicant/) 11 | 12 | Motivation 13 | ---------- 14 | 15 | This library provides an interface to the wpa_supplicant D-Bus interface. As of now, 16 | there is really no good option for interfacing with wpa_supplicant from Python and 17 | to go a step further, the existing D-Bus libraries are difficult to work with. This 18 | library abstracts all of that away into a very clean API based on [wpa_supplicant 19 | D-Bus documentation](http://w1.fi/wpa_supplicant/devel/dbus.html). 20 | 21 | 22 | Overview 23 | -------- 24 | 25 | This library is more than just a library that can be used programmatically, it also 26 | serves as a command-line tool for easily using wpa_supplicant and exercising the library. 27 | 28 | Here is a quick example of creating an wireless interface (wlan0) and performing a scan. 29 | 30 | With the library: 31 | 32 | ```py 33 | from wpa_supplicant.core import WpaSupplicantDriver 34 | from twisted.internet.selectreactor import SelectReactor 35 | import threading 36 | import time 37 | 38 | # Start a simple Twisted SelectReactor 39 | reactor = SelectReactor() 40 | threading.Thread(target=reactor.run, kwargs={'installSignalHandlers': 0}).start() 41 | time.sleep(0.1) # let reactor start 42 | 43 | # Start Driver 44 | driver = WpaSupplicantDriver(reactor) 45 | 46 | # Connect to the supplicant, which returns the "root" D-Bus object for wpa_supplicant 47 | supplicant = driver.connect() 48 | 49 | # Register an interface w/ the supplicant, this can raise an error if the supplicant 50 | # already knows about this interface 51 | interface = supplicant.create_interface('wlan0') 52 | 53 | # Issue the scan 54 | scan_results = interface.scan(block=True) 55 | for bss in scan_results: 56 | print bss.get_ssid() 57 | ``` 58 | 59 | With the CLI, you can do it in 2 commands: 60 | 61 | ``` 62 | $ wpa create_interface wlan0 63 | Interface(Path: /fi/w1/wpa_supplicant1/Interfaces/5, Name: wlan0, State: disconnected) 64 | OK 65 | $ wpa interface wlan0 scan 66 | BSS(Path: /fi/w1/wpa_supplicant1/Interfaces/3/BSSs/13, SSID: WINKHUB-107, BSSID: B4:79:A7:17:38:B5, Signal: -75dBm), 67 | BSS(Path: /fi/w1/wpa_supplicant1/Interfaces/3/BSSs/20, SSID: Stage, BSSID: 04:18:D6:67:2A:9C, Signal: -67dBm), 68 | BSS(Path: /fi/w1/wpa_supplicant1/Interfaces/3/BSSs/22, SSID: DAP-GUEST, BSSID: 00:07:7D:34:DB:BD, Signal: -73dBm), 69 | BSS(Path: /fi/w1/wpa_supplicant1/Interfaces/3/BSSs/24, SSID: HaberHive, BSSID: E0:1C:41:B5:6F:D5, Signal: -71dBm), 70 | BSS(Path: /fi/w1/wpa_supplicant1/Interfaces/3/BSSs/25, SSID: CMLS Guest WiFi, BSSID: 02:18:4A:91:E9:50, Signal: -79dBm), 71 | BSS(Path: /fi/w1/wpa_supplicant1/Interfaces/3/BSSs/26, SSID: Shout Public, BSSID: 0A:18:D6:67:27:9E, Signal: -77dBm), 72 | BSS(Path: /fi/w1/wpa_supplicant1/Interfaces/3/BSSs/29, SSID: ThinkTank, BSSID: 00:12:5F:03:AD:B4, Signal: -79dBm)] 73 | OK 74 | ``` 75 | 76 | 77 | Installation 78 | ------------ 79 | 80 | ```sh 81 | $ pip install wpa_supplicant 82 | ``` 83 | 84 | 85 | Feature Coverage Map 86 | -------------------- 87 | 88 | Often times adding a command or method to the library is very trivial however due to 89 | the finicky-ness of D-Bus in my experience, it is better to have an explicit method 90 | in the library that maps to a method in the D-Bus interface, even if the implementation 91 | is very cookie-cutter. 92 | 93 | Here is the current feature coverage: 94 | 95 | 96 | |Object | Method | Supported| 97 | |---------------------|---------------------------------|-----------| 98 | |root | CreateInterface | Yes | 99 | | | RemoveInterface | Yes | 100 | | | GetInterface | Yes | 101 | | | Get (properties) | Yes | 102 | | | Set (properties) | Yes | 103 | | | Register (signal) | Yes | 104 | |Interface | Scan | Yes | 105 | | | Disconnect | Yes | 106 | | | AddNetwork | Yes | 107 | | | RemoveNetwork | Yes | 108 | | | RemoveAllNetworks | Yes | 109 | | | SelectNetwork | Yes | 110 | | | Reassociate | * | 111 | | | Reattach | * | 112 | | | AddBlob | * | 113 | | | RemoveBlob | * | 114 | | | GetBlob | * | 115 | | | AutoScan | * | 116 | | | TDLSDiscover | * | 117 | | | TDLSSetup | * | 118 | | | TDLSStatus | * | 119 | | | TDLSTeardown | * | 120 | | | EAPLogoff | * | 121 | | | EAPLogon | * | 122 | | | NetworkReply | * | 123 | | | SetPKCS11EngineAndModulePath | * | 124 | | | SignalPoll | * | 125 | | | FlushBSS | * | 126 | | | SubscribeProbReq | * | 127 | | | UnsubscribeProbReq | * | 128 | | | Get (properties) | Yes | 129 | | | Set (properties) | Yes | 130 | | | Register (signal) | Yes | 131 | |Interface.WPS | Start | * | 132 | | | Get (properties) | * | 133 | | | Set (properties) | * | 134 | | | Register (signal) | * | 135 | |Interface.P2PDevice | Find | * | 136 | | | StopFind | * | 137 | | | Listen | * | 138 | | | ExtendedListen | * | 139 | | | PresenceRequest | * | 140 | | | ProvisionDiscoveryRequest | * | 141 | | | Connect | * | 142 | | | GroupAdd | * | 143 | | | Invite | * | 144 | | | Disconnect | * | 145 | | | RejectPeer | * | 146 | | | Flush | * | 147 | | | AddService | * | 148 | | | DeleteService | * | 149 | | | FlushService | * | 150 | | | ServiceDiscoveryRequest | * | 151 | | | ServiceDiscoveryResponse | * | 152 | | | ServiceDiscoveryCancelRequest | * | 153 | | | ServiceUpdate | * | 154 | | | Register (signal) | * | 155 | |BSS | Get (properties) | Yes | 156 | | | Set (properties) | Yes | 157 | | | Register (signal) | Yes | 158 | |Network | Get (properties) | Yes | 159 | | | Set (properties) | Yes | 160 | | | Register (signal) | Yes | 161 | |Peer | Get (properties) | * | 162 | | | Set (properties) | * | 163 | | | Register (signal) | * | 164 | |Group | Get (properties) | * | 165 | | | Set (properties) | * | 166 | | | Register (signal) | * | 167 | |PersistentGroup | Get (properties) | * | 168 | | | Set (properties) | * | 169 | | | Register (signal) | * | 170 | 171 | License 172 | ------- 173 | 174 | This software is open-source. Copyright (c), Digi International Inc., 2015. 175 | 176 | This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. 177 | If a copy of the MPL was not distributed with this file, you can obtain one at 178 | http://mozilla.org/MPL/2.0/. 179 | 180 | Digi, Digi International, the Digi logo, the Digi website, and Digi 181 | Device Cloud are trademarks or registered trademarks of Digi 182 | International, Inc. in the United States and other countries 183 | worldwide. All other trademarks are the property of their respective 184 | owners. 185 | 186 | THE SOFTWARE AND RELATED TECHNICAL INFORMATION IS PROVIDED "AS IS" 187 | WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 188 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR 189 | PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL DIGI OR ITS 190 | SUBSIDIARIES BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 191 | WHETHER IN AN ACTION IN CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 192 | OF OR IN CONNECTION WITH THE SOFTWARE AND TECHNICAL INFORMATION 193 | HEREIN, INCLUDING ALL SOURCE AND OBJECT CODES, IRRESPECTIVE OF HOW IT 194 | IS USED. YOU AGREE THAT YOU ARE NOT PROHIBITED FROM RECEIVING THIS 195 | SOFTWARE AND TECHNICAL INFORMATION UNDER UNITED STATES AND OTHER 196 | APPLICABLE COUNTRY EXPORT CONTROL LAWS AND REGULATIONS AND THAT YOU 197 | WILL COMPLY WITH ALL APPLICABLE UNITED STATES AND OTHER COUNTRY EXPORT 198 | LAWS AND REGULATIONS WITH REGARD TO USE AND EXPORT OR RE-EXPORT OF THE 199 | SOFTWARE AND TECHNICAL INFORMATION. 200 | -------------------------------------------------------------------------------- /wpa_supplicant/test/mocks.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015 Digi International Inc. All Rights Reserved. 6 | 7 | """This module provides classes for mocking out the txdbus layer at a low level 8 | 9 | Mocking out things at this layer means that some of the txdbus code is being tested 10 | in addition to the wpa_supplicant library code. Although a-typical of traditional unit 11 | testing this approach proved beneficial in practice. 12 | """ 13 | 14 | from twisted.internet import defer 15 | from collections import OrderedDict 16 | from txdbus import client, error 17 | import mock 18 | import re 19 | 20 | 21 | def init(): 22 | """Call at the beginning of ``testCase.setUp`` to mock D-Bus Layer""" 23 | 24 | conn = MockConnection() 25 | 26 | @defer.inlineCallbacks 27 | def mock_connect(reactor, busAddress): 28 | yield 29 | defer.returnValue(conn) 30 | 31 | client.connect = mock_connect 32 | 33 | 34 | class MockDBusObject(object): 35 | """Base class for mocked txdbus objects which implement D-Bus interfaces""" 36 | 37 | def __init__(self, *args, **kwargs): 38 | self.callRemote = mock.Mock(side_effect=self._callRemote) 39 | self.notifyOnSignal = mock.Mock(side_effect=self._notifyOnSignal) 40 | self.cancelSignalNotification = mock.Mock( 41 | side_effect=self._cancelSignalNotification) 42 | 43 | self._signals = dict() 44 | 45 | def _callRemote(self, method_name, *args): 46 | if method_name == 'Get': 47 | property_name = args[1] 48 | property_method = getattr(self, 'Get_%s' % property_name) 49 | if property_method: 50 | deferred = mock.Mock() 51 | deferred.result = property_method() 52 | deferred.called = True 53 | return deferred 54 | else: 55 | return None 56 | 57 | elif method_name == 'Set': 58 | return NotImplementedError() 59 | 60 | else: 61 | native_method = getattr(self, method_name) 62 | if native_method: 63 | deferred = mock.Mock() 64 | deferred.result = native_method(*args) 65 | deferred.called = True 66 | return deferred 67 | 68 | def _notifyOnSignal(self, signal_name, callback, interface=None): 69 | d = defer.Deferred() 70 | d.addCallback(callback) 71 | self._signals.setdefault(signal_name, list()).append(d) 72 | return d 73 | 74 | def _cancelSignalNotification(self, rule_id): 75 | pass 76 | 77 | def fire_signal(self, signal_name, value): 78 | deferreds = self._signals.get(signal_name, None) 79 | if deferreds is not None: 80 | for d in deferreds: 81 | d.callback(value) 82 | 83 | 84 | class MockWpaSupplicant(MockDBusObject): 85 | """Mock wpa_supplicant `Root` object""" 86 | 87 | def __init__(self, *args, **kwargs): 88 | MockDBusObject.__init__(self, *args, **kwargs) 89 | self._valid_interfaces = { 90 | 'wlan0': '/fi/w1/wpa_supplicant1/Interfaces/3', 91 | 'sta0': '/fi/w1/wpa_supplicant1/Interfaces/4' 92 | } 93 | self._created_interfaces = [] 94 | 95 | # 96 | # Methods 97 | # 98 | def GetInterface(self, interface_name): 99 | if interface_name in self._valid_interfaces: 100 | return self._valid_interfaces.get(interface_name) 101 | else: 102 | raise error.RemoteError('fi.w1.wpa_supplicant1.InterfaceUnknown') 103 | 104 | def RemoveInterface(self, interface_path): 105 | for i, path in enumerate(self._created_interfaces[:]): 106 | if path == interface_path: 107 | del self._created_interfaces[i] 108 | break 109 | else: 110 | raise error.RemoteError('fi.w1.wpa_supplicant1.InterfaceUnknown') 111 | 112 | def CreateInterface(self, cfg): 113 | if not isinstance(cfg, dict): 114 | raise error.RemoteError('fi.w1.wpa_supplicant1.InvalidArgs') 115 | 116 | interface_name = cfg.get('Ifname', None) # required argument 117 | if interface_name is None: 118 | raise error.RemoteError('fi.w1.wpa_supplicant1.InvalidArgs') 119 | 120 | iface_path = self._valid_interfaces.get(interface_name) 121 | 122 | if not iface_path: 123 | raise error.RemoteError('fi.w1.wpa_supplicant1.UnknownError') 124 | 125 | if iface_path in self._created_interfaces: 126 | raise error.RemoteError('fi.w1.wpa_supplicant1.InterfaceExists') 127 | 128 | self._created_interfaces.append(iface_path) 129 | return iface_path 130 | 131 | # 132 | # Properties 133 | # 134 | def Get_Interfaces(self): 135 | return [u'/fi/w1/wpa_supplicant1/Interfaces/7'] 136 | 137 | def Get_EapMethods(self): 138 | return [u'MD5', 139 | u'TLS', 140 | u'MSCHAPV2', 141 | u'PEAP', 142 | u'TTLS', 143 | u'GTC', 144 | u'OTP', 145 | u'SIM', 146 | u'LEAP', 147 | u'PSK', 148 | u'AKA', 149 | u"AKA'", 150 | u'FAST', 151 | u'PAX', 152 | u'SAKE', 153 | u'GPSK', 154 | u'WSC', 155 | u'IKEV2', 156 | u'TNC', 157 | u'PWD'] 158 | 159 | def Get_DebugLevel(self): 160 | return u'info' 161 | 162 | def Get_DebugShowKeys(self): 163 | return False 164 | 165 | def Get_DebugTimestamp(self): 166 | return False 167 | 168 | 169 | class MockInterfaceObject(MockDBusObject): 170 | """Mock wpa_supplicant `Interface` object""" 171 | 172 | def __init__(self, *args, **kwargs): 173 | MockDBusObject.__init__(self, *args, **kwargs) 174 | 175 | # Internal State 176 | self._network_counter = -1 177 | self._networks = dict() 178 | self._current_network = None 179 | 180 | # 181 | # Methods 182 | # 183 | def Scan(self, scan_config): 184 | return None 185 | 186 | def AddNetwork(self, cfg): 187 | self._network_counter += 1 188 | network_path = '/fi/w1/wpa_supplicant1/Networks/%s' % self._network_counter 189 | self._networks[network_path] = cfg 190 | return network_path 191 | 192 | def RemoveNetwork(self, network_path): 193 | if network_path in self._networks: 194 | del self._networks[network_path] 195 | else: 196 | raise error.RemoteError('fi.w1.wpa_supplicant1.NetworkUnknown') 197 | 198 | def SelectNetwork(self, network_path): 199 | self._current_network = network_path 200 | 201 | def Disconnect(self): 202 | if self._current_network is None: 203 | raise error.RemoteError('fi.w1.wpa_supplicant1.NotConnected') 204 | else: 205 | self._current_network = None 206 | 207 | # 208 | # Properties 209 | # 210 | def Get_Networks(self): 211 | return [] 212 | 213 | def Get_FastReauth(self): 214 | return True 215 | 216 | def Get_ScanInterval(self): 217 | return 5 218 | 219 | def Get_CurrentNetwork(self): 220 | return self._current_network 221 | 222 | def Get_Ifname(self): 223 | return u'wlan0' 224 | 225 | def Get_BSSs(self): 226 | return ['/fi/w1/wpa_supplicant1/Interfaces/3/BSSs/1234', ] 227 | 228 | def Get_CurrentBSS(self): 229 | return '/fi/w1/wpa_supplicant1/Interfaces/3/BSSs/1234' 230 | 231 | def Get_ApScan(self): 232 | return 1 233 | 234 | def Get_Scanning(self): 235 | return False 236 | 237 | def Get_State(self): 238 | return u'inactive' 239 | 240 | def Get_Capabilities(self): 241 | return {u'AuthAlg': [u'open', u'shared', u'leap'], 242 | u'Group': [u'ccmp', u'tkip', u'wep104', u'wep40'], 243 | u'KeyMgmt': [u'none', 244 | u'ieee8021x', 245 | u'wpa-eap', 246 | u'wpa-ft-eap', 247 | u'wpa-eap-sha256', 248 | u'wpa-psk', 249 | u'wpa-ft-psk', 250 | u'wpa-psk-sha256', 251 | u'wps'], 252 | u'MaxScanSSID': 4, 253 | u'Modes': [u'infrastructure', u'ad-hoc', u'ap'], 254 | u'Pairwise': [u'ccmp', u'tkip'], 255 | u'Protocol': [u'rsn', u'wpa'], 256 | u'Scan': [u'active', u'passive', u'ssid']} 257 | 258 | def Get_Country(self): 259 | return u'US' 260 | 261 | def Get_BSSExpireAge(self): 262 | return 180 263 | 264 | def Get_BSSExpireCount(self): 265 | return 2 266 | 267 | def Get_BridgeIfname(self): 268 | return u'' 269 | 270 | def Get_Driver(self): 271 | return u'nl80211' 272 | 273 | 274 | class MockBSSObject(MockDBusObject): 275 | """Mock txdbus/wpa_supplicant .fi.w1.wpa_supplicant1.BSS object""" 276 | 277 | # 278 | # Properties 279 | # 280 | def Get_BSSID(self): 281 | # This should return an array of bytes, for testing it is ideal 282 | # if the bytes fall within the ASCII range 283 | return [70, 71, 72, 73] 284 | 285 | def Get_SSID(self): 286 | # This should return an array of bytes, for testing it is ideal 287 | # if the bytes fall within the ASCII range 288 | return [70, 71, 72, 73] 289 | 290 | def Get_WPA(self): 291 | return {u'Group': u'tkip', u'KeyMgmt': [u'wpa-psk'], u'Pairwise': [u'tkip']} 292 | 293 | def Get_RSN(self): 294 | return {u'Group': u'', u'KeyMgmt': [], u'Pairwise': []} 295 | 296 | def Get_IEs(self): 297 | return [0, 298 | 9, 299 | 68, 300 | 65, 301 | 80, 302 | 150, 303 | 24, 304 | 36] 305 | 306 | def Get_Privacy(self): 307 | return True 308 | 309 | def Get_Mode(self): 310 | return u'infrastructure' 311 | 312 | def Get_Frequency(self): 313 | return 2462 314 | 315 | def Get_Rates(self): 316 | return [54000000, 48000000, 6000000] 317 | 318 | def Get_Signal(self): 319 | return -60 320 | 321 | 322 | class MockNetworkObject(MockDBusObject): 323 | """Mock txdbus/wpa_supplicant .fi.w1.wpa_supplicant1.Networks object""" 324 | 325 | def Get_Properties(self): 326 | return { 327 | u'ap_max_inactivity': u'0', 328 | u'beacon_int': u'0', 329 | u'bg_scan_period': u'-1', 330 | u'disabled': u'0', 331 | u'dtim_period': u'0', 332 | u'eap_workaround': u'-1', 333 | u'eapol_flags': u'3', 334 | u'engine': u'0', 335 | u'engine2': u'0', 336 | u'fragment_size': u'1398', 337 | u'frequency': u'0', 338 | u'group': u'CCMP TKIP WEP104 WEP40', 339 | u'ignore_broadcast_ssid': u'0', 340 | u'key_mgmt': u'WPA-PSK WPA-EAP', 341 | u'mixed_cell': u'0', 342 | u'mode': u'0', 343 | u'ocsp': u'0', 344 | u'pairwise': u'CCMP TKIP', 345 | u'peerkey': u'0', 346 | u'priority': u'0', 347 | u'proactive_key_caching': u'-1', 348 | u'proto': u'WPA RSN', 349 | u'scan_ssid': u'0', 350 | u'ssid': u'"wdnu-dvt1"', 351 | u'wep_tx_keyidx': u'0', 352 | u'wpa_ptk_rekey': u'0' 353 | } 354 | 355 | 356 | class MockConnection(object): 357 | """Mock txdbus client connection to a D-Bus server""" 358 | 359 | mock_objects = OrderedDict([ 360 | ('/fi/w1/wpa_supplicant1/Interfaces/.+/BSSs/.+', MockBSSObject), 361 | ('/fi/w1/wpa_supplicant1/Networks/.+', MockNetworkObject), 362 | ('/fi/w1/wpa_supplicant1/Interfaces/.+', MockInterfaceObject), 363 | ('/fi/w1/wpa_supplicant1', MockWpaSupplicant) 364 | ]) 365 | 366 | @defer.inlineCallbacks 367 | def getRemoteObject(self, busName, objectPath, interfaces=None): 368 | interface_object = None 369 | for opath, interface in self.mock_objects.items(): 370 | match = re.match(opath, objectPath) 371 | if match: 372 | interface_object = interface() 373 | break 374 | yield 375 | defer.returnValue(interface_object) 376 | -------------------------------------------------------------------------------- /wpa_supplicant/test/test_core.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015 Digi International Inc. All Rights Reserved. 6 | import time 7 | import unittest 8 | import threading 9 | 10 | import six 11 | from six.moves.queue import Queue 12 | from wpa_supplicant.core import WpaSupplicantDriver, Interface, BSS, Network, \ 13 | InterfaceUnknown, InterfaceExists, NotConnected, NetworkUnknown, WpaSupplicant 14 | from twisted.internet.selectreactor import SelectReactor 15 | import mock 16 | from wpa_supplicant.test import mocks 17 | 18 | 19 | class Task(object): 20 | def __init__(self, fn, *args, **kwargs): 21 | self._fn = fn 22 | self._args = args 23 | self._kwargs = kwargs 24 | 25 | def do(self): 26 | self._fn(*self._args, **self._kwargs) 27 | 28 | 29 | class ThreadedTaskRunner(threading.Thread): 30 | _QUIT = object() 31 | 32 | def __init__(self): 33 | threading.Thread.__init__(self) 34 | self.setDaemon(True) 35 | 36 | self._tasks = Queue() 37 | 38 | def run(self): 39 | while True: 40 | task = self._tasks.get() 41 | if task is self._QUIT: 42 | break 43 | else: 44 | if isinstance(task, Task): 45 | task.do() 46 | 47 | def stop(self): 48 | self._tasks.put(self._QUIT) 49 | 50 | def queue(self, task): 51 | self._tasks.put(task) 52 | 53 | 54 | class TestWpaSupplicant(unittest.TestCase): 55 | def setUp(self): 56 | mocks.init() 57 | self._taskrunner = ThreadedTaskRunner() 58 | self._taskrunner.start() 59 | self._reactor = SelectReactor() 60 | self._driver = WpaSupplicantDriver(self._reactor) 61 | self._reactor_thread = threading.Thread(target=self._reactor.run, 62 | kwargs={'installSignalHandlers': 0}) 63 | self._reactor_thread.start() 64 | time.sleep(0.1) 65 | self._supplicant = self._driver.connect() 66 | 67 | def tearDown(self): 68 | self._reactor.disconnectAll() 69 | self._reactor.sigTerm() 70 | self._reactor_thread.join() 71 | self._taskrunner.stop() 72 | 73 | # 74 | # Helpers 75 | # 76 | def _get_interface(self, interface_name): 77 | return self._supplicant.get_interface(interface_name) 78 | 79 | def _get_any_bss(self): 80 | iface = self._get_interface('wlan0') 81 | return BSS('/fi/w1/wpa_supplicant1/Interfaces/1/BSSs/1', 82 | iface._conn, 83 | iface._reactor) 84 | 85 | # 86 | # Test Driver 87 | # 88 | def test_connect(self): 89 | supplicant = self._driver.connect() 90 | self.assertIsInstance(supplicant, WpaSupplicant) 91 | 92 | # 93 | # Test Base 94 | # 95 | def test_register_for_signal(self): 96 | cb = mock.Mock() 97 | self._supplicant.register_signal('mysig', cb) 98 | self._supplicant._introspection.fire_signal('mysig', True) 99 | cb.assert_called_once_with(True) 100 | 101 | def test_register_for_signal_once(self): 102 | def fire_signal(result): 103 | time.sleep(1) 104 | self._supplicant._introspection.fire_signal('mysig', result) 105 | 106 | deferred_queue = self._supplicant.register_signal_once('mysig') 107 | self._taskrunner.queue(Task(fire_signal, True)) 108 | result = deferred_queue.get() 109 | self.assertEqual(result, True) 110 | 111 | # 112 | # Test Supplicant 113 | # 114 | def test_get_interface(self): 115 | interface = self._supplicant.get_interface('wlan0') 116 | self._supplicant._without_introspection.callRemote.assert_called_with( 117 | 'GetInterface', 'wlan0') 118 | self.assertTrue(isinstance(interface, Interface)) 119 | self.assertEqual(interface.get_path(), '/fi/w1/wpa_supplicant1/Interfaces/3') 120 | 121 | def test_get_unknown_interface(self): 122 | self.assertRaises(InterfaceUnknown, self._supplicant.get_interface, 'wlan99') 123 | 124 | def test_create_interface(self): 125 | interface = self._supplicant.create_interface('wlan0') 126 | self._supplicant._without_introspection.callRemote.assert_called_with( 127 | 'CreateInterface', 128 | {'Ifname': 'wlan0', 129 | 'Driver': None}) 130 | self.assertTrue(isinstance(interface, Interface)) 131 | self.assertEqual(interface.get_path(), '/fi/w1/wpa_supplicant1/Interfaces/3') 132 | 133 | def test_create_interface_already_exists(self): 134 | self.test_create_interface() 135 | self.assertRaises(InterfaceExists, self._supplicant.create_interface, 'wlan0') 136 | 137 | def test_remove_interface(self): 138 | self._supplicant.create_interface('wlan0') 139 | returnval = self._supplicant.remove_interface( 140 | '/fi/w1/wpa_supplicant1/Interfaces/3') 141 | self._supplicant._without_introspection.callRemote.assert_called_with( 142 | 'RemoveInterface', 143 | '/fi/w1/wpa_supplicant1/Interfaces/3') 144 | self.assertEqual(returnval, None) 145 | 146 | def test_remove_unknown_interface(self): 147 | supplicant = self._driver.connect() 148 | self.assertRaises(InterfaceUnknown, supplicant.remove_interface, 'wlan99') 149 | 150 | def test_get_debug_level(self): 151 | supplicant = self._driver.connect() 152 | self.assertEqual(supplicant.get_debug_level(), six.u('info')) 153 | 154 | def test_get_debug_timestamp(self): 155 | supplicant = self._driver.connect() 156 | self.assertEqual(supplicant.get_debug_timestamp(), False) 157 | 158 | def test_get_debug_showkeys(self): 159 | supplicant = self._driver.connect() 160 | 161 | def test_get_interfaces(self): 162 | supplicant = self._driver.connect() 163 | self.assertEqual(supplicant.get_interfaces(), 164 | [u'/fi/w1/wpa_supplicant1/Interfaces/7']) 165 | 166 | def test_get_eap_methods(self): 167 | supplicant = self._driver.connect() 168 | self.assertEqual(supplicant.get_eap_methods(), [u'MD5', 169 | u'TLS', 170 | u'MSCHAPV2', 171 | u'PEAP', 172 | u'TTLS', 173 | u'GTC', 174 | u'OTP', 175 | u'SIM', 176 | u'LEAP', 177 | u'PSK', 178 | u'AKA', 179 | u"AKA'", 180 | u'FAST', 181 | u'PAX', 182 | u'SAKE', 183 | u'GPSK', 184 | u'WSC', 185 | u'IKEV2', 186 | u'TNC', 187 | u'PWD']) 188 | 189 | # 190 | # Test Interface 191 | # 192 | def test_interface_scan(self): 193 | interface = self._get_interface('wlan0') 194 | scan_results = interface.scan() 195 | self.assertEqual(scan_results, None) 196 | 197 | def test_interface_blocking_scan(self): 198 | interface = self._get_interface('wlan0') 199 | 200 | def fire_signal(): 201 | time.sleep(1) 202 | interface._introspection.fire_signal('ScanDone', True) 203 | 204 | self._taskrunner.queue(Task(fire_signal)) 205 | scan_results = interface.scan(block=True) 206 | for res in scan_results: 207 | self.assertTrue(isinstance(res, BSS)) 208 | self.assertEqual(res.get_path(), 209 | '/fi/w1/wpa_supplicant1/Interfaces/3/BSSs/1234') 210 | 211 | def test_add_network(self): 212 | interface = self._get_interface('wlan0') 213 | network = interface.add_network({}) 214 | self.assertTrue(isinstance(network, Network)) 215 | self.assertEqual(network.get_path(), '/fi/w1/wpa_supplicant1/Networks/0') 216 | 217 | def test_remove_network(self): 218 | interface = self._get_interface('wlan0') 219 | network = interface.add_network({}) 220 | result = interface.remove_network(network.get_path()) 221 | self.assertEqual(result, None) 222 | 223 | def test_remove_unknown_network(self): 224 | interface = self._get_interface('wlan0') 225 | self.assertRaises(NetworkUnknown, 226 | interface.remove_network, 227 | '/fi/w1/wpa_supplicant1/Networks/44') 228 | 229 | def test_select_network(self): 230 | interface = self._get_interface('wlan0') 231 | network = interface.add_network({}) 232 | interface.select_network(network.get_path()) 233 | current_network = interface.get_current_network() 234 | self.assertEqual(current_network.get_path(), network.get_path()) 235 | 236 | def test_get_ifname(self): 237 | interface = self._get_interface('wlan0') 238 | self.assertEqual(interface.get_ifname(), 'wlan0') 239 | 240 | def test_get_current_bss(self): 241 | interface = self._get_interface('wlan0') 242 | bss = interface.get_current_bss() 243 | self.assertTrue(isinstance(bss, BSS)) 244 | 245 | def test_get_current_network(self): 246 | interface = self._get_interface('wlan0') 247 | net = interface.get_current_network() 248 | self.assertEqual(net, None) 249 | 250 | def test_network_disconnect(self): 251 | interface = self._get_interface('wlan0') 252 | network = interface.add_network({}) 253 | interface.select_network(network.get_path()) 254 | interface.disconnect_network() 255 | self.assertIsNone(interface.get_current_network()) 256 | 257 | def test_network_disconnect_not_connected(self): 258 | interface = self._get_interface('wlan0') 259 | self.assertRaises(NotConnected, interface.disconnect_network) 260 | 261 | def test_get_networks(self): 262 | interface = self._get_interface('wlan0') 263 | self.assertEqual(interface.get_networks(), []) 264 | 265 | def test_get_state(self): 266 | interface = self._get_interface('wlan0') 267 | self.assertEqual(interface.get_state(), u'inactive') 268 | 269 | def test_get_scanning(self): 270 | interface = self._get_interface('wlan0') 271 | self.assertEqual(interface.get_scanning(), False) 272 | 273 | def test_get_scan_interval(self): 274 | interface = self._get_interface('wlan0') 275 | self.assertEqual(interface.get_scan_interval(), 5) 276 | 277 | def test_get_fast_reauth(self): 278 | interface = self._get_interface('wlan0') 279 | self.assertEqual(interface.get_fast_reauth(), True) 280 | 281 | def test_get_all_bss(self): 282 | interface = self._get_interface('wlan0') 283 | self.assertEqual(interface.get_all_bss(), 284 | ['/fi/w1/wpa_supplicant1/Interfaces/3/BSSs/1234', ]) 285 | 286 | def test_get_driver(self): 287 | interface = self._get_interface('wlan0') 288 | self.assertEqual(interface.get_driver(), u'nl80211') 289 | 290 | def test_get_country(self): 291 | interface = self._get_interface('wlan0') 292 | self.assertEqual(interface.get_country(), u'US') 293 | 294 | def test_get_bridge_ifname(self): 295 | interface = self._get_interface('wlan0') 296 | self.assertEqual(interface.get_bridge_ifname(), u'') 297 | 298 | def test_get_bss_expire_age(self): 299 | interface = self._get_interface('wlan0') 300 | self.assertEqual(interface.get_bss_expire_age(), 180) 301 | 302 | def test_get_bss_expire_count(self): 303 | interface = self._get_interface('wlan0') 304 | self.assertEqual(interface.get_bss_expire_count(), 2) 305 | 306 | def test_get_ap_scan(self): 307 | interface = self._get_interface('wlan0') 308 | self.assertEqual(interface.get_ap_scan(), 1) 309 | 310 | # 311 | # Test BSS 312 | # 313 | def test_get_channel(self): 314 | # The "channel" is a conversion from frequency, so we need to test 315 | # the calculation being used 316 | bss = self._get_any_bss() 317 | self.assertEqual(bss.get_channel(), 11) 318 | 319 | def test_get_ssid(self): 320 | bss = self._get_any_bss() 321 | self.assertEqual(bss.get_ssid(), 'FGHI') 322 | 323 | def test_get_bssid(self): 324 | bss = self._get_any_bss() 325 | self.assertEqual(bss.get_bssid(), '46:47:48:49') 326 | 327 | def test_get_frequency(self): 328 | bss = self._get_any_bss() 329 | self.assertEqual(bss.get_frequency(), 2462) 330 | 331 | def test_get_wpa(self): 332 | bss = self._get_any_bss() 333 | self.assertEqual(bss.get_wpa(), 334 | {u'Group': u'tkip', u'KeyMgmt': [u'wpa-psk'], u'Pairwise': [u'tkip']}) 335 | 336 | def test_get_rsn(self): 337 | bss = self._get_any_bss() 338 | self.assertEqual(bss.get_rsn(), {u'Group': u'', u'KeyMgmt': [], u'Pairwise': []}) 339 | 340 | def test_get_ies(self): 341 | bss = self._get_any_bss() 342 | self.assertEqual(bss.get_ies(), [0, 9, 68, 65, 80, 150, 24, 36]) 343 | 344 | def test_get_privacy(self): 345 | interface = self._get_interface('wlan0') 346 | bss = interface.get_current_bss() 347 | self.assertEqual(bss.get_privacy(), True) 348 | 349 | def test_get_mode(self): 350 | bss = self._get_any_bss() 351 | self.assertEqual(bss.get_mode(), u'infrastructure') 352 | 353 | def test_get_rates(self): 354 | bss = self._get_any_bss() 355 | self.assertEqual(bss.get_rates(), [54000000, 48000000, 6000000]) 356 | 357 | def test_get_signal_dbm(self): 358 | interface = self._get_interface('wlan0') 359 | bss = interface.get_current_bss() 360 | self.assertEqual(-60, bss.get_signal_dbm()) 361 | 362 | def test_get_signal_quality(self): 363 | interface = self._get_interface('wlan0') 364 | bss = interface.get_current_bss() 365 | self.assertEqual(80, bss.get_signal_quality()) 366 | 367 | # 368 | # Test Network 369 | # 370 | def test_get_properties(self): 371 | interface = self._get_interface('wlan0') 372 | desired_network = interface.add_network({'ssid': 'foo', 'psk': 'bar'}) 373 | interface.select_network(desired_network.get_path()) 374 | curr_network = interface.get_current_network() 375 | props = curr_network.get_properties() 376 | self.assertEqual(props['ssid'], 'wdnu-dvt1') 377 | 378 | def test_get_enabled(self): 379 | pass 380 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. -------------------------------------------------------------------------------- /wpa_supplicant/core.py: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public 2 | # License, v. 2.0. If a copy of the MPL was not distributed with this 3 | # file, You can obtain one at http://mozilla.org/MPL/2.0/. 4 | # 5 | # Copyright (c) 2015 Digi International Inc. All Rights Reserved. 6 | 7 | """ 8 | 9 | This module (library) is an intuitive wrapper around wpa_supplicant's D-Bus API 10 | 11 | For the most part, the library aims to provide a one-to-one mapping into the D-Bus 12 | interfaces offered by wpa_supplicant. Certain abstractions or niceties have been made 13 | to make the usage of the API more intuitive. 14 | 15 | The libraries API was mainly derived from the following documentation: 16 | 17 | http://w1.fi/wpa_supplicant/devel/dbus.html 18 | 19 | """ 20 | from twisted.internet import defer, threads 21 | from txdbus.interface import DBusInterface, Method, Signal 22 | from txdbus import client, error 23 | from functools import wraps 24 | import threading 25 | import logging 26 | 27 | # 28 | # Constants/Globals 29 | # 30 | BUS_NAME = 'fi.w1.wpa_supplicant1' 31 | logger = logging.getLogger('wpasupplicant') 32 | 33 | 34 | # 35 | # Helpers 36 | # 37 | def _catch_remote_errors(fn): 38 | """Decorator for catching and wrapping txdbus.error.RemoteError exceptions""" 39 | @wraps(fn) 40 | def closure(*args, **kwargs): 41 | try: 42 | return fn(*args, **kwargs) 43 | except error.RemoteError as ex: 44 | match = _REMOTE_EXCEPTIONS.get(str(ex.errName)) 45 | if match is not None: 46 | raise match(ex.message) 47 | else: 48 | logger.warn("Unrecognized error: %s" % ex.errName) 49 | raise WpaSupplicantException(ex.message) 50 | return closure 51 | 52 | 53 | def _eval(deferred, reactor): 54 | """Evaluate a deferred on a given reactor and return the result 55 | 56 | This function is safe to call with a deferred that has already been evaluated. 57 | """ 58 | 59 | @defer.inlineCallbacks 60 | def closure(): 61 | if deferred.called: 62 | result = deferred.result 63 | else: 64 | result = yield deferred 65 | 66 | defer.returnValue(result) 67 | 68 | if threading.currentThread().getName() == reactor.thread_name: 69 | return closure() 70 | else: 71 | return threads.blockingCallFromThread(reactor, closure) 72 | 73 | 74 | # 75 | # Exceptions 76 | # 77 | class WpaSupplicantException(Exception): 78 | """Base class for all exception types raised from this lib""" 79 | 80 | 81 | class UnknownError(WpaSupplicantException): 82 | """Something failed for an unknown reason 83 | 84 | Possible sources: 85 | Basically everything in this library 86 | """ 87 | 88 | 89 | class MethodTimeout(WpaSupplicantException): 90 | """Raised when a timeout occurs while waiting on DeferredQueue.get() 91 | 92 | Possible sources: 93 | :meth:`~Interface.scan` with block=True 94 | """ 95 | 96 | 97 | class InterfaceExists(WpaSupplicantException): 98 | """wpa_supplicant already controls this interface 99 | 100 | Possible sources: 101 | :meth:`~WpaSupplicant.create_interface` 102 | """ 103 | 104 | 105 | class InvalidArgs(WpaSupplicantException): 106 | """Invalid entries were found in the passed arguments 107 | 108 | Possible sources: 109 | :meth:`~WpaSupplicant.create_interface` 110 | :meth:`~Interface.scan` 111 | :meth:`~Interface.add_network` 112 | :meth:`~Interface.remove_network` 113 | :meth:`~Interface.select_network` 114 | """ 115 | 116 | 117 | class InterfaceUnknown(WpaSupplicantException): 118 | """Object pointed by the path doesn't exist or doesn't represent an interface 119 | 120 | Possible sources: 121 | :meth:`~WpaSupplicant.get_interface` 122 | :meth:`~WpaSupplicant.remove_interface` 123 | """ 124 | 125 | 126 | class NotConnected(WpaSupplicantException): 127 | """Interface is not connected to any network 128 | 129 | Possible sources: 130 | :meth:`~Interface.disconnect` 131 | """ 132 | 133 | 134 | class NetworkUnknown(WpaSupplicantException): 135 | """A passed path doesn't point to any network object 136 | 137 | Possible sources: 138 | :meth:`~Interface.remove_network` 139 | :meth:`~Interface.select_network` 140 | """ 141 | 142 | 143 | class ReactorNotRunning(WpaSupplicantException): 144 | """In order to connect to the WpaSupplicantDriver a reactor must be started""" 145 | 146 | 147 | # These are exceptions defined by the wpa_supplicant D-Bus API 148 | _REMOTE_EXCEPTIONS = { 149 | 'fi.w1.wpa_supplicant1.UnknownError': UnknownError, 150 | 'fi.w1.wpa_supplicant1.InvalidArgs': InvalidArgs, 151 | 'fi.w1.wpa_supplicant1.InterfaceExists': InterfaceExists, 152 | 'fi.w1.wpa_supplicant1.InterfaceUnknown': InterfaceUnknown, 153 | 'fi.w1.wpa_supplicant1.NotConnected': NotConnected, 154 | 'fi.w1.wpa_supplicant1.NetworkUnknown': NetworkUnknown, 155 | } 156 | 157 | 158 | # 159 | # Library Core 160 | # 161 | class TimeoutDeferredQueue(defer.DeferredQueue): 162 | """Deferred Queue implementation that provides .get() a timeout keyword arg""" 163 | 164 | def __init__(self, reactor): 165 | defer.DeferredQueue.__init__(self) 166 | self._reactor = reactor 167 | 168 | def _timed_out(self, deferred): 169 | """A timeout occurred while waiting for the deferred to be fired""" 170 | 171 | if not deferred.called: 172 | if deferred in self.waiting: 173 | self.waiting.remove(deferred) 174 | deferred.errback(MethodTimeout('Timed out waiting for response')) 175 | 176 | def _stop_timeout(self, result, call_id): 177 | """One of the callbacks in the deferreds callback-chain""" 178 | 179 | call_id.cancel() 180 | return result 181 | 182 | def get(self, timeout=None): 183 | """Synchronously get the result of :meth:`defer.DeferredQueue.get()` 184 | 185 | :param timeout: optional timeout parameter (integer # of sections) 186 | :returns: The result value that has been put onto the deferred queue 187 | :raises MethodTimeout: More time has elapsed than the specified `timeout` 188 | """ 189 | 190 | deferred = defer.DeferredQueue.get(self) 191 | 192 | if timeout: 193 | call_id = self._reactor.callLater(timeout, self._timed_out, deferred) 194 | deferred.addCallback(self._stop_timeout, call_id) 195 | 196 | return _eval(deferred, self._reactor) 197 | 198 | 199 | class RemoteSignal(object): 200 | """Encapsulation of a signal object returned by signal registration methods""" 201 | 202 | def __init__(self, signal_name, callback, remote_object, remote_interface, reactor): 203 | self._signal_name = signal_name 204 | self._callback = callback 205 | self._remote_object = remote_object 206 | self._remote_interface = remote_interface 207 | self._reactor = reactor 208 | self._deferred = remote_object.notifyOnSignal(signal_name, callback, remote_interface) 209 | 210 | def get_signal_name(self): 211 | return self._signal_name 212 | 213 | def cancel(self): 214 | self._remote_object.cancelSignalNotification(_eval(self._deferred, self._reactor)) 215 | 216 | 217 | class BaseIface(object): 218 | """Base class for all Interfaces defined in the wpa_supplicant D-Bus API""" 219 | 220 | def __init__(self, obj_path, conn, reactor): 221 | self._obj_path = obj_path 222 | self._conn = conn 223 | self._reactor = reactor 224 | self._introspection = _eval(self._conn.getRemoteObject(BUS_NAME, self._obj_path), 225 | self._reactor) 226 | self._without_introspection = _eval(self._conn.getRemoteObject(BUS_NAME, self._obj_path, self.iface), 227 | self._reactor) 228 | 229 | def get_path(self): 230 | """Return the full D-Bus object path that defines this interface 231 | 232 | :returns: String D-Bus object path, e.g. /w1/fi/wpa_supplicant1/Interfaces/0 233 | :rtype: str 234 | """ 235 | 236 | return self._obj_path 237 | 238 | @_catch_remote_errors 239 | def _call_remote(self, method, *args): 240 | """Call a remote D-Bus interface method using introspection""" 241 | 242 | return _eval(self._introspection.callRemote(method, *args), self._reactor) 243 | 244 | @_catch_remote_errors 245 | def _call_remote_without_introspection(self, method, *args): 246 | """Call a remote D-Bus interface method without using introspection""" 247 | 248 | return _eval(self._without_introspection.callRemote(method, *args), self._reactor) 249 | 250 | @_catch_remote_errors 251 | def register_signal_once(self, signal_name): 252 | """Register a signal on an Interface object 253 | 254 | .. note:: 255 | 256 | Since this de-registers itself after the signal fires the only real 257 | use for this function is code that wants to block and wait for a signal 258 | to fire, hence the queue it returns. 259 | 260 | :param signal_name: Case-sensitive name of the signal 261 | :returns: Object of type :class:`~EventSignal` 262 | """ 263 | 264 | q = TimeoutDeferredQueue(self._reactor) 265 | 266 | signal = None 267 | 268 | def signal_callback(result): 269 | q.put(result) 270 | if signal is not None: 271 | signal.cancel() 272 | 273 | signal = RemoteSignal(signal_name, 274 | signal_callback, 275 | self._introspection, 276 | self.INTERFACE_PATH, 277 | self._reactor) 278 | 279 | return q 280 | 281 | @_catch_remote_errors 282 | def register_signal(self, signal_name, callback): 283 | """Register a callback when a signal fires 284 | 285 | :param signal_name: Case-sensitve name of the signal 286 | :param callback: Callable object 287 | :returns: ~`RemoteSignal` 288 | """ 289 | if not callable(callback): 290 | raise WpaSupplicantException('callback must be callable') 291 | 292 | return RemoteSignal(signal_name, 293 | callback, 294 | self._introspection, 295 | self.INTERFACE_PATH, 296 | self._reactor) 297 | 298 | @_catch_remote_errors 299 | def get(self, property_name): 300 | """Get the value of a property defined by the interface 301 | 302 | :param property_name: Case-sensitive string of the property name 303 | :returns: Variant type of the properties value 304 | """ 305 | return self._call_remote('Get', self.INTERFACE_PATH, property_name) 306 | 307 | @_catch_remote_errors 308 | def set(self, property_name, value): 309 | """Set the value of a property defined by the interface 310 | 311 | :param property_name: Case-sensitive string of the property name 312 | :param value: Variant type value to set for property 313 | :returns: `None` 314 | """ 315 | 316 | logger.info('Setting `%s` -> `%s`', property_name, value) 317 | return self._call_remote('Set', self.INTERFACE_PATH, property_name, value) 318 | 319 | 320 | class WpaSupplicant(BaseIface): 321 | """Interface implemented by the main wpa_supplicant D-Bus object 322 | 323 | Registered in the bus as "fi.w1.wpa_supplicant1" 324 | """ 325 | 326 | INTERFACE_PATH = 'fi.w1.wpa_supplicant1' 327 | 328 | iface = DBusInterface( 329 | INTERFACE_PATH, 330 | Method('CreateInterface', arguments='a{sv}', returns='o'), 331 | Method('GetInterface', arguments='s', returns='o'), 332 | Method('RemoveInterface', arguments='o'), 333 | Signal('InterfaceAdded', 'o,a{sv}'), 334 | Signal('InterfaceRemoved', 'o'), 335 | Signal('PropertiesChanged', 'a{sv}') 336 | ) 337 | 338 | def __init__(self, *args, **kwargs): 339 | BaseIface.__init__(self, *args, **kwargs) 340 | self._interfaces_cache = dict() 341 | 342 | def __repr__(self): 343 | return str(self) 344 | 345 | def __str__(self): 346 | return "WpaSupplicant(Interfaces: %s)" % self.get_interfaces() 347 | 348 | # 349 | # Methods 350 | # 351 | def get_interface(self, interface_name): 352 | """Get D-Bus object related to an interface which wpa_supplicant already controls 353 | 354 | :returns: Interface object that implements the wpa_supplicant Interface API. 355 | :rtype: :class:`~Interface` 356 | :raises InterfaceUnknown: wpa_supplicant doesn't know anything about `interface_name` 357 | :raises UnknownError: An unknown error occurred 358 | """ 359 | 360 | interface_path = self._call_remote_without_introspection('GetInterface', 361 | interface_name) 362 | interface = self._interfaces_cache.get(interface_path, None) 363 | if interface is not None: 364 | return interface 365 | else: 366 | interface = Interface(interface_path, self._conn, self._reactor) 367 | self._interfaces_cache[interface_path] = interface 368 | return interface 369 | 370 | def create_interface(self, interface_name, driver=None): 371 | """Registers a wireless interface in wpa_supplicant 372 | 373 | :returns: Interface object that implements the wpa_supplicant Interface API 374 | :raises InterfaceExists: The `interface_name` specified is already registered 375 | :raises UnknownError: An unknown error occurred 376 | """ 377 | 378 | interface_path = self._call_remote_without_introspection('CreateInterface', 379 | {'Ifname': interface_name, 380 | 'Driver': driver}) 381 | return Interface(interface_path, self._conn, self._reactor) 382 | 383 | def remove_interface(self, interface_path): 384 | """Deregisters a wireless interface from wpa_supplicant 385 | 386 | :param interface_path: D-Bus object path to the interface to be removed 387 | :returns: None 388 | :raises InterfaceUnknown: wpa_supplicant doesn't know anything about `interface_name` 389 | :raises UnknownError: An unknown error occurred 390 | """ 391 | 392 | self._call_remote_without_introspection('RemoveInterface', interface_path) 393 | 394 | # 395 | # Properties 396 | # 397 | def get_debug_level(self): 398 | """Global wpa_supplicant debugging level 399 | 400 | :returns: Possible values: "msgdump" (verbose debugging) 401 | "debug" (debugging) 402 | "info" (informative) 403 | "warning" (warnings) 404 | "error" (errors) 405 | :rtype: str 406 | """ 407 | 408 | return self.get('DebugLevel') 409 | 410 | def get_debug_timestamp(self): 411 | """ Determines if timestamps are shown in debug logs""" 412 | 413 | return self.get('DebugTimestamp') 414 | 415 | def get_debug_showkeys(self): 416 | """Determines if secrets are shown in debug logs""" 417 | 418 | return self.get('DebugShowKeys') 419 | 420 | def get_interfaces(self): 421 | """An array with paths to D-Bus objects representing controlled interfaces""" 422 | 423 | return self.get('Interfaces') 424 | 425 | def get_eap_methods(self): 426 | """An array with supported EAP methods names""" 427 | 428 | return self.get('EapMethods') 429 | 430 | 431 | class Interface(BaseIface): 432 | """Interface implemented by objects related to network interface added to wpa_supplicant""" 433 | 434 | INTERFACE_PATH = 'fi.w1.wpa_supplicant1.Interface' 435 | 436 | iface = DBusInterface( 437 | INTERFACE_PATH, 438 | Method('Scan', arguments='a{sv}'), 439 | Method('AddNetwork', arguments='a{sv}', returns='o'), 440 | Method('RemoveNetwork', arguments='o'), 441 | Method('SelectNetwork', arguments='o'), 442 | Method('Disconnect'), 443 | Method('SaveConfig'), 444 | Signal('ScanDone', 'b'), 445 | Signal('PropertiesChanged', 'a{sv}') 446 | ) 447 | 448 | def __repr__(self): 449 | return str(self) 450 | 451 | def __str__(self): 452 | return "Interface(Path: %s, Name: %s, State: %s)" % (self.get_path(), 453 | self.get_ifname(), 454 | self.get_state()) 455 | 456 | # 457 | # Methods 458 | # 459 | def scan(self, type='active', ssids=None, ies=None, channels=None, block=False): 460 | """Triggers a scan 461 | 462 | :returns: List of `BSS` objects if block=True else None 463 | :raises InvalidArgs: Invalid argument format 464 | :raises MethodTimeout: Scan has timed out (only if block=True) 465 | :raises UnknownError: An unknown error occurred 466 | """ 467 | 468 | # TODO: Handle the other arguments 469 | scan_options = { 470 | 'Type': type 471 | } 472 | 473 | # If blocking, register for the ScanDone signal and return BSSs 474 | if block: 475 | deferred_queue = self.register_signal_once('ScanDone') 476 | self._call_remote('Scan', scan_options) # Trigger scan 477 | success = deferred_queue.get(timeout=10) 478 | if success: 479 | return [BSS(path, self._conn, self._reactor) for path in self.get_all_bss()] 480 | else: 481 | raise UnknownError('ScanDone signal received without success') 482 | else: 483 | self._call_remote('Scan', scan_options) # Trigger scan 484 | 485 | def add_network(self, network_cfg): 486 | """Adds a new network to the interface 487 | 488 | :param network_cfg: Dictionary of config, see wpa_supplicant.conf for k/v pairs 489 | :returns: `Network` object that was registered w/ wpa_supplicant 490 | :raises InvalidArgs: Invalid argument format 491 | :raises UnknownError: An unknown error occurred 492 | """ 493 | 494 | network_path = self._call_remote('AddNetwork', network_cfg) 495 | return Network(network_path, self._conn, self._reactor) 496 | 497 | def remove_network(self, network_path): 498 | """Removes a configured network from the interface 499 | 500 | :param network_path: D-Bus object path to the desired network 501 | :returns: None 502 | :raises NetworkUnknown: The specified `network_path` is invalid 503 | :raises InvalidArgs: Invalid argument format 504 | :raises UnknownError: An unknown error occurred 505 | """ 506 | 507 | self._call_remote('RemoveNetwork', network_path) 508 | 509 | def select_network(self, network_path): 510 | """Attempt association with a configured network 511 | 512 | :param network_path: D-Bus object path to the desired network 513 | :returns: None 514 | :raises NetworkUnknown: The specified `network_path` has not been added 515 | :raises InvalidArgs: Invalid argument format 516 | """ 517 | 518 | self._call_remote('SelectNetwork', network_path) 519 | 520 | def disconnect_network(self): 521 | """Disassociates the interface from current network 522 | 523 | :returns: None 524 | :raises NotConnected: The interface is not currently connected to a network 525 | """ 526 | 527 | self._call_remote('Disconnect') 528 | 529 | def save_config(self): 530 | """Save current configuration to disk 531 | 532 | :returns: None 533 | """ 534 | 535 | self._call_remote('SaveConfig') 536 | 537 | # 538 | # Properties 539 | # 540 | def get_ifname(self): 541 | """Name of network interface controlled by the interface, e.g., wlan0""" 542 | 543 | return self.get('Ifname') 544 | 545 | def get_current_bss(self): 546 | """BSS object path which wpa_supplicant is associated with 547 | 548 | Returns "/" if is not associated at all 549 | """ 550 | 551 | bss_path = self.get('CurrentBSS') 552 | if bss_path == '/' or bss_path is None: 553 | return None 554 | else: 555 | return BSS(bss_path, self._conn, self._reactor) 556 | 557 | def get_current_network(self): 558 | """The `Network` object which wpa_supplicant is associated with 559 | 560 | Returns `None` if is not associated at all 561 | """ 562 | 563 | network_path = self.get('CurrentNetwork') 564 | if network_path == '/' or network_path is None: 565 | return None 566 | else: 567 | return Network(network_path, self._conn, self._reactor) 568 | 569 | def get_networks(self): 570 | """List of `Network` objects representing configured networks""" 571 | 572 | networks = list() 573 | paths = self.get('Networks') 574 | for network_path in paths: 575 | if network_path == '/': 576 | networks.append(None) 577 | else: 578 | networks.append(Network(network_path, self._conn, self._reactor)) 579 | return networks 580 | 581 | def get_state(self): 582 | """A state of the interface. 583 | 584 | Possible values are: "disconnected" 585 | "inactive" 586 | "scanning" 587 | "authenticating" 588 | "associating" 589 | "associated" 590 | "4way_handshake" 591 | "group_handshake" 592 | "completed" 593 | "unknown" 594 | """ 595 | 596 | return self.get('State') 597 | 598 | def get_scanning(self): 599 | """Determines if the interface is already scanning or not""" 600 | 601 | return self.get('Scanning') 602 | 603 | def get_scan_interval(self): 604 | """Time (in seconds) between scans for a suitable AP. Must be >= 0""" 605 | 606 | return self.get('ScanInterval') 607 | 608 | def get_fast_reauth(self): 609 | """Identical to fast_reauth entry in wpa_supplicant.conf""" 610 | 611 | return self.get('FastReauth') 612 | 613 | def get_all_bss(self): 614 | """List of D-Bus objects paths representing BSSs known to the interface""" 615 | 616 | return self.get('BSSs') 617 | 618 | def get_driver(self): 619 | """Name of driver used by the interface, e.g., nl80211""" 620 | 621 | return self.get('Driver') 622 | 623 | def get_country(self): 624 | """Identical to country entry in wpa_supplicant.conf""" 625 | 626 | return self.get('Country') 627 | 628 | def get_bridge_ifname(self): 629 | """Name of bridge network interface controlled by the interface, e.g., br0""" 630 | 631 | return self.get('BridgeIfname') 632 | 633 | def get_bss_expire_age(self): 634 | """Identical to bss_expiration_age entry in wpa_supplicant.conf file""" 635 | 636 | return self.get('BSSExpireAge') 637 | 638 | def get_bss_expire_count(self): 639 | """Identical to bss_expiration_scan_count entry in wpa_supplicant.conf file""" 640 | 641 | return self.get('BSSExpireCount') 642 | 643 | def get_ap_scan(self): 644 | """Identical to ap_scan entry in wpa_supplicant configuration file. 645 | 646 | Possible values are 0, 1 or 2. 647 | """ 648 | 649 | return self.get('ApScan') 650 | 651 | def set_country(self, country_code): 652 | self.set('Country', country_code) 653 | 654 | 655 | class BSS(BaseIface): 656 | """Interface implemented by objects representing a scanned BSSs (scan results)""" 657 | 658 | INTERFACE_PATH = 'fi.w1.wpa_supplicant1.BSS' 659 | 660 | iface = DBusInterface( 661 | INTERFACE_PATH, 662 | Signal('PropertiesChanged', 'a{sv}') 663 | ) 664 | 665 | def __repr__(self): 666 | return str(self) 667 | 668 | def __str__(self): 669 | return "BSS(Path: %s, SSID: %s, BSSID: %s, Signal: %sdBm)" % (self.get_path(), 670 | self.get_ssid(), 671 | self.get_bssid(), 672 | self.get_signal_dbm()) 673 | 674 | def to_dict(self): 675 | """Dict representation of a BSS object""" 676 | 677 | elements = ( 678 | ('ssid', self.get_ssid), 679 | ('rsn', self.get_rsn), 680 | ('channel', self.get_channel), 681 | ('privacy', self.get_privacy), 682 | ('wpa', self.get_wpa), 683 | ('signal_dbm', self.get_signal_dbm), 684 | ('signal_quality', self.get_signal_quality), 685 | ('network_type', self.get_network_type), 686 | ('privacy', self.get_privacy), 687 | ) 688 | out = {} 689 | for k, v in elements: 690 | try: 691 | out[k] = v() 692 | except: 693 | logger.exception('Error while fetching BSS information') 694 | return out 695 | 696 | # 697 | # Properties 698 | # 699 | def get_channel(self): 700 | """Wi-Fi channel number (1-14)""" 701 | freq = self.get_frequency() 702 | 703 | if freq == 2484: # Handle channel 14 704 | return 14 705 | elif freq >= 2412 and freq <= 2472: 706 | return 1 + (freq - 2412) / 5 707 | elif freq >= 5180 and freq <= 5905: 708 | return 36 + (freq - 5180) / 5 709 | else: 710 | logger.warn('Unexpected frequency %s', freq) 711 | raise WpaSupplicantException('Unexpected frequency in WiFi connection.') 712 | 713 | def get_ssid(self): 714 | """SSID of the BSS in ASCII""" 715 | 716 | return "".join(chr(i) for i in self.get('SSID')) 717 | 718 | def get_bssid(self): 719 | """BSSID of the BSS as hex bytes delimited by a colon""" 720 | 721 | return ":".join(["{:02X}".format(i) for i in self.get('BSSID')]) 722 | 723 | def get_frequency(self): 724 | """Frequency of the BSS in MHz""" 725 | 726 | return self.get('Frequency') 727 | 728 | def get_wpa(self): 729 | """WPA information of the BSS, empty dictionary indicates no WPA support 730 | 731 | Dictionaries are:: 732 | 733 | { 734 | "KeyMgmt": , 735 | "Pairwise": , 736 | "Group": , 737 | "MgmtGroup": 738 | } 739 | """ 740 | 741 | return self.get('WPA') 742 | 743 | def get_rsn(self): 744 | """RSN/WPA2 information of the BSS, empty dictionary indicates no RSN support 745 | 746 | Dictionaries are:: 747 | 748 | { 749 | "KeyMgmt": , 750 | "Pairwise": , 751 | "Group": , 752 | "MgmtGroup": , 753 | } 754 | """ 755 | 756 | return self.get('RSN') 757 | 758 | def get_ies(self): 759 | """All IEs of the BSS as a chain of TLVs""" 760 | 761 | return self.get('IEs') 762 | 763 | def get_privacy(self): 764 | """Indicates if BSS supports privacy""" 765 | 766 | return self.get('Privacy') 767 | 768 | def get_mode(self): 769 | """Describes mode of the BSS 770 | 771 | Possible values are: "ad-hoc" 772 | "infrastructure" 773 | 774 | """ 775 | 776 | return self.get('Mode') 777 | 778 | def get_rates(self): 779 | """Descending ordered array of rates supported by the BSS in bits per second""" 780 | 781 | return self.get('Rates') 782 | 783 | def get_signal_dbm(self): 784 | """Signal strength of the BSS in dBm""" 785 | 786 | return self.get('Signal') 787 | 788 | def get_signal_quality(self): 789 | """Signal strength of the BSS as a percentage (0-100)""" 790 | 791 | dbm = self.get_signal_dbm() 792 | if dbm <= -100: 793 | return 0 794 | elif dbm >= -50: 795 | return 100 796 | else: 797 | return 2 * (dbm + 100) 798 | 799 | def get_network_type(self): 800 | """Return the network type as a string 801 | 802 | Possible values are: 803 | 'WPA' 804 | 'WPA2' 805 | 'WEP' 806 | 'OPEN' 807 | """ 808 | 809 | if self.get_privacy(): 810 | rsn_key_mgmt = self.get_rsn().get('KeyMgmt') 811 | if rsn_key_mgmt: 812 | return 'WPA2' 813 | 814 | wpa_key_mgmt = self.get_wpa().get('KeyMgmt') 815 | if wpa_key_mgmt: 816 | return 'WPA' 817 | 818 | return 'WEP' 819 | else: 820 | return 'OPEN' 821 | 822 | 823 | class Network(BaseIface): 824 | """Interface implemented by objects representing configured networks""" 825 | 826 | INTERFACE_PATH = 'fi.w1.wpa_supplicant1.Network' 827 | 828 | iface = DBusInterface( 829 | INTERFACE_PATH, 830 | Signal('PropertiesChanged', 'a{sv}') 831 | ) 832 | 833 | def __repr__(self): 834 | return str(self) 835 | 836 | def __str__(self): 837 | return "Network(Path: %s, Properties: %s)" % (self.get_path(), 838 | self.get_properties()) 839 | 840 | # 841 | # Properties 842 | # 843 | def get_properties(self): 844 | """Properties of the configured network 845 | 846 | Dictionary contains entries from "network" block of wpa_supplicant.conf. 847 | All values are string type, e.g., frequency is "2437", not 2437. 848 | """ 849 | 850 | network_properties = self.get('Properties') 851 | ssid = network_properties.get('ssid', '') 852 | if ssid: 853 | quote_chars = {'"', "'"} 854 | ssid = ssid[1:] if ssid[0] in quote_chars else ssid 855 | ssid = ssid[:-1] if ssid[-1] in quote_chars else ssid 856 | network_properties.update({ 857 | 'ssid': ssid 858 | }) 859 | return network_properties 860 | 861 | def get_enabled(self): 862 | """Determines if the configured network is enabled or not""" 863 | 864 | return self.get('Enabled') 865 | 866 | 867 | class WpaSupplicantDriver(object): 868 | """Driver object for starting, stopping and connecting to wpa_supplicant""" 869 | 870 | def __init__(self, reactor): 871 | self._reactor = reactor 872 | 873 | @_catch_remote_errors 874 | def connect(self): 875 | """Connect to wpa_supplicant over D-Bus 876 | 877 | :returns: Remote D-Bus proxy object of the root wpa_supplicant interface 878 | :rtype: :class:`~WpaSupplicant` 879 | """ 880 | 881 | if not self._reactor.running: 882 | raise ReactorNotRunning('Twisted Reactor must be started (call .run())') 883 | 884 | @defer.inlineCallbacks 885 | def get_conn(): 886 | self._reactor.thread_name = threading.currentThread().getName() 887 | conn = yield client.connect(self._reactor, busAddress='system') 888 | defer.returnValue(conn) 889 | 890 | conn = threads.blockingCallFromThread(self._reactor, get_conn) 891 | return WpaSupplicant('/fi/w1/wpa_supplicant1', conn, self._reactor, ) 892 | --------------------------------------------------------------------------------