├── .gitignore ├── .travis.yml ├── LICENSE ├── MANIFEST.in ├── README.rst ├── bash_completion ├── bin └── wifi ├── docs ├── Makefile ├── conf.py ├── index.rst ├── scanning.rst └── wifi_command.rst ├── setup.py ├── tests ├── __init__.py └── test_schemes.py └── wifi ├── __init__.py ├── pbkdf2.py ├── scan.py ├── scheme.py ├── subprocess_compat.py └── utils.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.egg* 3 | build/ 4 | dist/ 5 | docs/_build 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.6" 4 | - "2.7" 5 | - "3.3" 6 | # command to install dependencies 7 | install: 8 | - python setup.py -q install 9 | # command to run tests 10 | script: python setup.py test 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Gavin Wahl , Rocky Meza 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | softwARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | include bash_completion 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | wifi 2 | ---- 3 | 4 | WiFi tools that could possibly work on a nice day, if you are lucky. 5 | 6 | :: 7 | 8 | # pip install wifi 9 | # wifi --help 10 | 11 | 12 | .. image:: https://travis-ci.org/rockymeza/wifi.png?branch=master 13 | :target: https://travis-ci.org/rockymeza/wifi 14 | 15 | The documentation for wifi lives at https://wifi.readthedocs.org/en/latest/. 16 | -------------------------------------------------------------------------------- /bash_completion: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | _wifi() 4 | { 5 | local cur opts 6 | COMPREPLY=() 7 | cur="${COMP_WORDS[COMP_CWORD]}" 8 | 9 | opts=$( COMP_CWORD=$COMP_CWORD \ 10 | COMP_WORDS="${COMP_WORDS[*]}" \ 11 | WIFI_AUTOCOMPLETE=1 \ 12 | $1 ) 13 | 14 | COMPREPLY=($(compgen -W "$opts" -- ${cur})) 15 | 16 | return 0 17 | } 18 | 19 | complete -F _wifi wifi 20 | -------------------------------------------------------------------------------- /bin/wifi: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | from __future__ import print_function 3 | import argparse 4 | import sys 5 | import os 6 | 7 | from wifi import Cell, Scheme 8 | from wifi.utils import print_table, match as fuzzy_match 9 | 10 | 11 | def find_cell(interface, query): 12 | match_partial = lambda cell: fuzzy_match(query, cell.ssid) 13 | 14 | matches = Cell.where(interface, match_partial) 15 | 16 | num_matches = len(set(cell.ssid for cell in matches)) 17 | assert num_matches > 0, "Couldn't find a network that matches '{}'".format(query) 18 | assert num_matches < 2, "Found more than one network that matches '{}'".format(query) 19 | 20 | return matches[0] 21 | 22 | 23 | def scheme_for_ssid(interface, scheme, ssid=None): 24 | cell = find_cell(interface, ssid or scheme) 25 | 26 | passkey = None if not cell.encrypted else raw_input('wpa passkey> ') 27 | 28 | return Scheme.for_cell(interface, scheme, cell, passkey) 29 | 30 | 31 | def scan_command(args): 32 | print_table([[cell.signal, cell.ssid, 'protected' if cell.encrypted else 'unprotected'] for cell in Cell.all(args.interface)]) 33 | 34 | 35 | def list_command(args): 36 | for scheme in Scheme.all(): 37 | print(scheme.name) 38 | 39 | 40 | def show_command(args): 41 | scheme = scheme_for_ssid(args.interface, args.scheme, args.ssid) 42 | print(scheme) 43 | 44 | 45 | def add_command(args): 46 | assert not Scheme.find(args.interface, args.scheme), "That scheme has already been used" 47 | 48 | scheme = scheme_for_ssid(args.interface, args.scheme, args.ssid) 49 | scheme.save() 50 | 51 | 52 | def connect_command(args): 53 | if args.adhoc: 54 | # ensure that we have the adhoc utility scheme 55 | try: 56 | adhoc_scheme = Scheme(args.interface, 'adhoc') 57 | adhoc_scheme.save() 58 | except AssertionError: 59 | pass 60 | 61 | scheme = scheme_for_ssid(args.interface, 'adhoc', args.scheme) 62 | else: 63 | scheme = Scheme.find(args.interface, args.scheme) 64 | 65 | scheme.activate() 66 | 67 | 68 | parser = argparse.ArgumentParser() 69 | parser.add_argument('-i', 70 | '--interface', 71 | default='wlan0', 72 | help="Specifies which interface to use (wlan0, eth0, etc.)") 73 | 74 | subparsers = parser.add_subparsers(title='commands') 75 | 76 | parser_scan = subparsers.add_parser('scan', help="Shows a list of available networks.") 77 | parser_scan.set_defaults(func=scan_command) 78 | 79 | parser_list = subparsers.add_parser('list', help="Shows a list of networks already configured.") 80 | parser_list.set_defaults(func=list_command) 81 | 82 | scheme_help = ("A memorable nickname for a wireless network." 83 | " If SSID is not provided, the network will be guessed using SCHEME.") 84 | ssid_help = ("The SSID for the network to which you wish to connect." 85 | " This is fuzzy matched, so you don't have to be precise.") 86 | 87 | parser_show = subparsers.add_parser('config', 88 | help="Prints the configuration to connect to a new network.") 89 | parser_show.add_argument('scheme', help=scheme_help, metavar='SCHEME') 90 | parser_show.add_argument('ssid', nargs='?', help=ssid_help, metavar='SSID') 91 | parser_show.set_defaults(func=show_command) 92 | 93 | parser_add = subparsers.add_parser('add', 94 | help="Adds the configuration to connect to a new network.") 95 | parser_add.add_argument('scheme', help=scheme_help, metavar='SCHEME') 96 | parser_add.add_argument('ssid', nargs='?', help=ssid_help, metavar='SSID') 97 | parser_add.set_defaults(func=add_command) 98 | 99 | parser_connect = subparsers.add_parser('connect', 100 | help="Connects to the network corresponding to SCHEME") 101 | parser_connect.add_argument('scheme', 102 | help="The nickname of the network to which you wish to connect.", 103 | metavar='SCHEME') 104 | parser_connect.add_argument('-a', 105 | '--ad-hoc', 106 | dest='adhoc', 107 | action="store_true", 108 | help="Connect to a network without storing it in the config file") 109 | parser_connect.set_defaults(func=connect_command) 110 | 111 | 112 | parser_connect.options = [scheme.name for scheme in Scheme.all()] 113 | 114 | 115 | def autocomplete(position, wordlist): 116 | if position == 1: 117 | ret = subparsers.choices.keys() 118 | else: 119 | try: 120 | prev = wordlist[position - 1] 121 | ret = subparsers.choices[prev].options 122 | except (IndexError, KeyError, AttributeError): 123 | ret = [] 124 | 125 | print(' '.join(ret)) 126 | 127 | if len(sys.argv) == 1: 128 | argv = ['scan'] 129 | else: 130 | argv = sys.argv[1:] 131 | 132 | args = parser.parse_args(argv) 133 | 134 | try: 135 | if 'WIFI_AUTOCOMPLETE' in os.environ: 136 | autocomplete(int(os.environ['COMP_CWORD']), os.environ['COMP_WORDS'].split()) 137 | else: 138 | args.func(args) 139 | except AssertionError as e: 140 | sys.stderr.write("Error: ") 141 | sys.exit(e.message) 142 | -------------------------------------------------------------------------------- /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 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 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 " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/wifi.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/wifi.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/wifi" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/wifi" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # wifi documentation build configuration file, created by 4 | # sphinx-quickstart on Mon Dec 24 14:32:32 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | #sys.path.insert(0, os.path.abspath('.')) 20 | 21 | # -- General configuration ----------------------------------------------------- 22 | 23 | # If your documentation needs a minimal Sphinx version, state it here. 24 | #needs_sphinx = '1.0' 25 | 26 | # Add any Sphinx extension module names here, as strings. They can be extensions 27 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 28 | extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.coverage', 'sphinx.ext.viewcode'] 29 | 30 | # Add any paths that contain templates here, relative to this directory. 31 | templates_path = ['_templates'] 32 | 33 | # The suffix of source filenames. 34 | source_suffix = '.rst' 35 | 36 | # The encoding of source files. 37 | #source_encoding = 'utf-8-sig' 38 | 39 | # The master toctree document. 40 | master_doc = 'index' 41 | 42 | # General information about the project. 43 | project = u'wifi' 44 | copyright = u'2012, Rocky Meza and Gavin Wahl' 45 | 46 | # The version info for the project you're documenting, acts as replacement for 47 | # |version| and |release|, also used in various other places throughout the 48 | # built documents. 49 | # 50 | # The short X.Y version. 51 | version = '0.0.1' 52 | # The full version, including alpha/beta/rc tags. 53 | release = '0.0.1' 54 | 55 | # The language for content autogenerated by Sphinx. Refer to documentation 56 | # for a list of supported languages. 57 | #language = None 58 | 59 | # There are two options for replacing |today|: either, you set today to some 60 | # non-false value, then it is used: 61 | #today = '' 62 | # Else, today_fmt is used as the format for a strftime call. 63 | #today_fmt = '%B %d, %Y' 64 | 65 | # List of patterns, relative to source directory, that match files and 66 | # directories to ignore when looking for source files. 67 | exclude_patterns = ['_build'] 68 | 69 | # The reST default role (used for this markup: `text`) to use for all documents. 70 | #default_role = None 71 | 72 | # If true, '()' will be appended to :func: etc. cross-reference text. 73 | #add_function_parentheses = True 74 | 75 | # If true, the current module name will be prepended to all description 76 | # unit titles (such as .. function::). 77 | #add_module_names = True 78 | 79 | # If true, sectionauthor and moduleauthor directives will be shown in the 80 | # output. They are ignored by default. 81 | #show_authors = False 82 | 83 | # The name of the Pygments (syntax highlighting) style to use. 84 | pygments_style = 'sphinx' 85 | 86 | # A list of ignored prefixes for module index sorting. 87 | #modindex_common_prefix = [] 88 | 89 | 90 | # -- Options for HTML output --------------------------------------------------- 91 | 92 | # The theme to use for HTML and HTML Help pages. See the documentation for 93 | # a list of builtin themes. 94 | html_theme = 'default' 95 | 96 | # Theme options are theme-specific and customize the look and feel of a theme 97 | # further. For a list of options available for each theme, see the 98 | # documentation. 99 | #html_theme_options = {} 100 | 101 | # Add any paths that contain custom themes here, relative to this directory. 102 | #html_theme_path = [] 103 | 104 | # The name for this set of Sphinx documents. If None, it defaults to 105 | # " v documentation". 106 | #html_title = None 107 | 108 | # A shorter title for the navigation bar. Default is the same as html_title. 109 | #html_short_title = None 110 | 111 | # The name of an image file (relative to this directory) to place at the top 112 | # of the sidebar. 113 | #html_logo = None 114 | 115 | # The name of an image file (within the static path) to use as favicon of the 116 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 117 | # pixels large. 118 | #html_favicon = None 119 | 120 | # Add any paths that contain custom static files (such as style sheets) here, 121 | # relative to this directory. They are copied after the builtin static files, 122 | # so a file named "default.css" will overwrite the builtin "default.css". 123 | html_static_path = ['_static'] 124 | 125 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 126 | # using the given strftime format. 127 | #html_last_updated_fmt = '%b %d, %Y' 128 | 129 | # If true, SmartyPants will be used to convert quotes and dashes to 130 | # typographically correct entities. 131 | #html_use_smartypants = True 132 | 133 | # Custom sidebar templates, maps document names to template names. 134 | #html_sidebars = {} 135 | 136 | # Additional templates that should be rendered to pages, maps page names to 137 | # template names. 138 | #html_additional_pages = {} 139 | 140 | # If false, no module index is generated. 141 | #html_domain_indices = True 142 | 143 | # If false, no index is generated. 144 | #html_use_index = True 145 | 146 | # If true, the index is split into individual pages for each letter. 147 | #html_split_index = False 148 | 149 | # If true, links to the reST sources are added to the pages. 150 | #html_show_sourcelink = True 151 | 152 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 153 | #html_show_sphinx = True 154 | 155 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 156 | #html_show_copyright = True 157 | 158 | # If true, an OpenSearch description file will be output, and all pages will 159 | # contain a tag referring to it. The value of this option must be the 160 | # base URL from which the finished HTML is served. 161 | #html_use_opensearch = '' 162 | 163 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 164 | #html_file_suffix = None 165 | 166 | # Output file base name for HTML help builder. 167 | htmlhelp_basename = 'wifidoc' 168 | 169 | 170 | # -- Options for LaTeX output -------------------------------------------------- 171 | 172 | latex_elements = { 173 | # The paper size ('letterpaper' or 'a4paper'). 174 | #'papersize': 'letterpaper', 175 | 176 | # The font size ('10pt', '11pt' or '12pt'). 177 | #'pointsize': '10pt', 178 | 179 | # Additional stuff for the LaTeX preamble. 180 | #'preamble': '', 181 | } 182 | 183 | # Grouping the document tree into LaTeX files. List of tuples 184 | # (source start file, target name, title, author, documentclass [howto/manual]). 185 | latex_documents = [ 186 | ('index', 'wifi.tex', u'wifi Documentation', 187 | u'Rocky Meza and Gavin Wahl', 'manual'), 188 | ] 189 | 190 | # The name of an image file (relative to this directory) to place at the top of 191 | # the title page. 192 | #latex_logo = None 193 | 194 | # For "manual" documents, if this is true, then toplevel headings are parts, 195 | # not chapters. 196 | #latex_use_parts = False 197 | 198 | # If true, show page references after internal links. 199 | #latex_show_pagerefs = False 200 | 201 | # If true, show URL addresses after external links. 202 | #latex_show_urls = False 203 | 204 | # Documents to append as an appendix to all manuals. 205 | #latex_appendices = [] 206 | 207 | # If false, no module index is generated. 208 | #latex_domain_indices = True 209 | 210 | 211 | # -- Options for manual page output -------------------------------------------- 212 | 213 | # One entry per manual page. List of tuples 214 | # (source start file, name, description, authors, manual section). 215 | man_pages = [ 216 | ('index', 'wifi', u'wifi Documentation', 217 | [u'Rocky Meza and Gavin Wahl'], 1) 218 | ] 219 | 220 | # If true, show URL addresses after external links. 221 | #man_show_urls = False 222 | 223 | 224 | # -- Options for Texinfo output ------------------------------------------------ 225 | 226 | # Grouping the document tree into Texinfo files. List of tuples 227 | # (source start file, target name, title, author, 228 | # dir menu entry, description, category) 229 | texinfo_documents = [ 230 | ('index', 'wifi', u'wifi Documentation', 231 | u'Rocky Meza and Gavin Wahl', 'wifi', 'One line description of project.', 232 | 'Miscellaneous'), 233 | ] 234 | 235 | # Documents to append as an appendix to all manuals. 236 | #texinfo_appendices = [] 237 | 238 | # If false, no module index is generated. 239 | #texinfo_domain_indices = True 240 | 241 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 242 | #texinfo_show_urls = 'footnote' 243 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | wifi, a Python interface 2 | ======================== 3 | 4 | Wifi provides a set of tools for configuring and connecting to WiFi networks on Linux systems. 5 | Using this library, you can discover networks, connect to them, save your configurations, and much, much more. 6 | 7 | The original impetus for creating this library was my frustration with with connecting to the Internet using NetworkManager and wicd. 8 | It is very much for computer programmers, not so much for normal computer users. 9 | Wifi is built on top the old technologies of the `/etc/network/interfaces` file and `ifup` and `ifdown`. 10 | It is inspired by `ifscheme`. 11 | 12 | The library also comes with an executable that you can use to manage your WiFi connections. 13 | 14 | Contents: 15 | 16 | .. toctree:: 17 | :maxdepth: 2 18 | 19 | wifi_command 20 | scanning 21 | 22 | 23 | 24 | Indices and tables 25 | ================== 26 | 27 | * :ref:`genindex` 28 | * :ref:`modindex` 29 | * :ref:`search` 30 | -------------------------------------------------------------------------------- /docs/scanning.rst: -------------------------------------------------------------------------------- 1 | Managing WiFi networks 2 | ====================== 3 | 4 | .. currentmodule:: wifi 5 | 6 | Discovering networks 7 | -------------------- 8 | 9 | You can use this library to scan for networks that are available in the air. 10 | To get a list of the different cells in the area, you can do :: 11 | 12 | >>> from wifi.scan import Cell 13 | >>> Cell.all('wlan0') 14 | 15 | This returns a list of :class:`Cell` objects. Under the hood, this calls `iwlist scan` and parses the unfriendly output. 16 | 17 | Each cell object should have the following attributes: 18 | 19 | - :attr:`ssid` 20 | - :attr:`frequency` 21 | - :attr:`bitrates` 22 | - :attr:`encrypted` 23 | - :attr:`channel` 24 | - :attr:`address` 25 | - :attr:`mode` 26 | 27 | For cells that have :attr:`encrypted` as `True`, there will also be the following attributes: 28 | 29 | - :attr:`encryption_type` 30 | 31 | .. note:: 32 | 33 | Scanning requires root permission to see all the networks. 34 | If you are not root, iwlist only returns the network you are currently connected to. 35 | 36 | 37 | Connecting to a network 38 | ----------------------- 39 | 40 | In order to connect to a network, you need to set up a scheme for it. :: 41 | 42 | >>> cell = Cell.all('wlan0')[0] 43 | >>> scheme = Scheme.for_cell('wlan0', 'home', cell) 44 | >>> scheme.save() 45 | >>> scheme.activate() 46 | 47 | Once you have a scheme saved, you can retrieve it using :meth:`Scheme.find`. :: 48 | 49 | >>> scheme = Scheme.find('wlan0', 'home') 50 | >>> scheme.activate() 51 | 52 | .. note:: 53 | 54 | Activating a scheme will disconnect from any other scheme before connecting. 55 | 56 | You must be root to connect to a network. 57 | Wifi uses `ifdown` and `ifup` to connect and disconnect. 58 | 59 | 60 | .. autoclass:: Cell 61 | :members: 62 | 63 | .. autoclass:: Scheme 64 | :members: 65 | -------------------------------------------------------------------------------- /docs/wifi_command.rst: -------------------------------------------------------------------------------- 1 | The wifi Command 2 | ================ 3 | 4 | This library comes with a command line program for managing and saving your WiFi connections. 5 | 6 | Usage 7 | ^^^^^ 8 | 9 | :: 10 | 11 | usage: wifi {scan,list,config,add,connect,init} ... 12 | 13 | scan 14 | ---- 15 | 16 | Shows a list of available networks. :: 17 | 18 | usage: wifi scan 19 | 20 | list 21 | ---- 22 | 23 | Shows a list of networks already configured. :: 24 | 25 | usage: wifi list 26 | 27 | add, config 28 | ----------- 29 | 30 | Prints or adds the configuration to connect to a new network. :: 31 | 32 | usage: wifi config SCHEME [SSID] 33 | usage: wifi add SCHEME [SSID] 34 | 35 | positional arguments: 36 | SCHEME A memorable nickname for a wireless network. If SSID is not 37 | provided, the network will be guessed using SCHEME. 38 | SSID The SSID for the network to which you wish to connect. This is 39 | fuzzy matched, so you don't have to be precise. 40 | 41 | connect 42 | ------- 43 | 44 | Connects to the network corresponding to SCHEME. :: 45 | 46 | usage: wifi connect SCHEME 47 | 48 | positional arguments: 49 | SCHEME The nickname of the network to which you wish to connect. 50 | 51 | 52 | Completion 53 | ^^^^^^^^^^ 54 | 55 | The wifi command also comes with bash completion. 56 | To use the completion, source the `bash_completion` file that comes with the distribution. 57 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | import os 4 | 5 | __doc__ = """ 6 | WiFi tools that could possibly work on a nice day, if you are lucky. 7 | """ 8 | 9 | 10 | def read(fname): 11 | return open(os.path.join(os.path.dirname(__file__), fname)).read() 12 | 13 | 14 | install_requires = ['setuptools'] 15 | try: 16 | import argparse 17 | except: 18 | install_requires.append('argparse') 19 | 20 | version = '0.1.0' 21 | 22 | setup( 23 | name='wifi', 24 | version=version, 25 | author='Rocky Meza, Gavin Wahl', 26 | author_email='rockymeza@gmail.com', 27 | description=__doc__, 28 | long_description=read('README.rst'), 29 | packages=['wifi'], 30 | scripts=['bin/wifi'], 31 | test_suite='tests', 32 | platforms="Debian", 33 | license='BSD', 34 | install_requires=install_requires, 35 | classifiers=[ 36 | "License :: OSI Approved :: BSD License", 37 | "Topic :: System :: Networking", 38 | "Operating System :: POSIX :: Linux", 39 | "Environment :: Console", 40 | ], 41 | ) 42 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CptMonac/python-wifi/5b1bd0f94cb13144d201db0f2f1235fbcd022282/tests/__init__.py -------------------------------------------------------------------------------- /tests/test_schemes.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | import tempfile 3 | 4 | from wifi.scheme import extract_schemes, Scheme 5 | 6 | 7 | NETWORK_INTERFACES_FILE = """ 8 | # This file describes the network interfaces available on your system 9 | # and how to activate them. For more information, see interfaces(5). 10 | 11 | # The loopback network interface 12 | auto lo 13 | iface lo inet loopback 14 | 15 | # The primary network interface 16 | allow-hotplug eth0 17 | iface eth0 inet dhcp 18 | 19 | iface wlan0-work inet dhcp 20 | wpa-ssid workwifi 21 | wpa-psk 1111111111111111111111111111111111111111111111111111111111111111 22 | wireless-channel auto 23 | 24 | iface wlan0-coffee inet dhcp 25 | wireless-essid Coffee WiFi 26 | wireless-channel auto 27 | 28 | iface wlan0-home inet dhcp 29 | wpa-ssid homewifi 30 | wpa-psk 2222222222222222222222222222222222222222222222222222222222222222 31 | wireless-channel auto 32 | 33 | iface wlan0-coffee2 inet dhcp 34 | wireless-essid Coffee 2 35 | wireless-channel auto 36 | """ 37 | 38 | 39 | class TestSchemes(TestCase): 40 | def setUp(self): 41 | self.tempfile, Scheme.interfaces = tempfile.mkstemp() 42 | 43 | with open(Scheme.interfaces, 'w') as f: 44 | f.write(NETWORK_INTERFACES_FILE) 45 | 46 | def test_scheme_extraction(self): 47 | work, coffee, home, coffee2 = extract_schemes(NETWORK_INTERFACES_FILE) 48 | 49 | assert work.name == 'work' 50 | assert work.options['wpa-ssid'] == 'workwifi' 51 | 52 | assert coffee.name == 'coffee' 53 | assert coffee.options['wireless-essid'] == 'Coffee WiFi' 54 | 55 | def test_str(self): 56 | scheme = Scheme('wlan0', 'test') 57 | assert str(scheme) == 'iface wlan0-test inet dhcp\n' 58 | 59 | scheme = Scheme('wlan0', 'test', { 60 | 'wpa-ssid': 'workwifi', 61 | }) 62 | 63 | self.assertEqual(str(scheme), 'iface wlan0-test inet dhcp\n wpa-ssid workwifi\n') 64 | 65 | def test_find(self): 66 | work = Scheme.find('wlan0', 'work') 67 | 68 | assert work.options['wpa-ssid'] == 'workwifi' 69 | 70 | def test_save(self): 71 | scheme = Scheme('wlan0', 'test') 72 | scheme.save() 73 | 74 | assert Scheme.find('wlan0', 'test') 75 | -------------------------------------------------------------------------------- /wifi/__init__.py: -------------------------------------------------------------------------------- 1 | from wifi.scan import Cell 2 | from wifi.scheme import Scheme 3 | -------------------------------------------------------------------------------- /wifi/pbkdf2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | pbkdf2 4 | ~~~~~~ 5 | 6 | This module implements pbkdf2 for Python. It also has some basic 7 | tests that ensure that it works. The implementation is straightforward 8 | and uses stdlib only stuff and can be easily be copy/pasted into 9 | your favourite application. 10 | 11 | Use this as replacement for bcrypt that does not need a c implementation 12 | of a modified blowfish crypto algo. 13 | 14 | Example usage: 15 | 16 | >>> pbkdf2_hex('what i want to hash', 'the random salt') 17 | 'fa7cc8a2b0a932f8e6ea42f9787e9d36e592e0c222ada6a9' 18 | 19 | How to use this: 20 | 21 | 1. Use a constant time string compare function to compare the stored hash 22 | with the one you're generating:: 23 | 24 | def safe_str_cmp(a, b): 25 | if len(a) != len(b): 26 | return False 27 | rv = 0 28 | for x, y in izip(a, b): 29 | rv |= ord(x) ^ ord(y) 30 | return rv == 0 31 | 32 | 2. Use `os.urandom` to generate a proper salt of at least 8 byte. 33 | Use a unique salt per hashed password. 34 | 35 | 3. Store ``algorithm$salt:costfactor$hash`` in the database so that 36 | you can upgrade later easily to a different algorithm if you need 37 | one. For instance ``PBKDF2-256$thesalt:10000$deadbeef...``. 38 | 39 | 40 | :copyright: (c) Copyright 2011 by Armin Ronacher. 41 | :license: BSD, see LICENSE for more details. 42 | """ 43 | import hmac 44 | import hashlib 45 | from struct import Struct 46 | from operator import xor 47 | from itertools import starmap 48 | try: 49 | from itertools import izip 50 | except ImportError: 51 | izip = zip 52 | 53 | 54 | _pack_int = Struct('>I').pack 55 | 56 | 57 | def pbkdf2_hex(data, salt, iterations=1000, keylen=24, hashfunc=None): 58 | """Like :func:`pbkdf2_bin` but returns a hex encoded string.""" 59 | return pbkdf2_bin(data, salt, iterations, keylen, hashfunc).encode('hex') 60 | 61 | 62 | def pbkdf2_bin(data, salt, iterations=1000, keylen=24, hashfunc=None): 63 | """Returns a binary digest for the PBKDF2 hash algorithm of `data` 64 | with the given `salt`. It iterates `iterations` time and produces a 65 | key of `keylen` bytes. By default SHA-1 is used as hash function, 66 | a different hashlib `hashfunc` can be provided. 67 | """ 68 | hashfunc = hashfunc or hashlib.sha1 69 | mac = hmac.new(data, None, hashfunc) 70 | def _pseudorandom(x, mac=mac): 71 | h = mac.copy() 72 | h.update(x) 73 | return map(ord, h.digest()) 74 | buf = [] 75 | for block in xrange(1, -(-keylen // mac.digest_size) + 1): 76 | rv = u = _pseudorandom(salt + _pack_int(block)) 77 | for i in xrange(iterations - 1): 78 | u = _pseudorandom(''.join(map(chr, u))) 79 | rv = starmap(xor, izip(rv, u)) 80 | buf.extend(rv) 81 | return ''.join(map(chr, buf))[:keylen] 82 | 83 | -------------------------------------------------------------------------------- /wifi/scan.py: -------------------------------------------------------------------------------- 1 | import re 2 | import textwrap 3 | 4 | import wifi.subprocess_compat as subprocess 5 | 6 | 7 | class Cell(object): 8 | """ 9 | Presents a Python interface to the output of iwlist. 10 | """ 11 | 12 | def __init__(self): 13 | self.bitrates = [] 14 | 15 | def __repr__(self): 16 | return 'Cell(ssid={ssid})'.format(**vars(self)) 17 | 18 | @classmethod 19 | def all(cls, interface): 20 | """ 21 | Returns a list of all cells extracted from the output of 22 | iwlist. 23 | """ 24 | iwlist_scan = subprocess.check_output(['/sbin/iwlist', interface, 'scan']).decode('utf-8') 25 | 26 | cells = map(normalize, cells_re.split(iwlist_scan)[1:]) 27 | 28 | return cells 29 | 30 | @classmethod 31 | def where(cls, interface, fn): 32 | """ 33 | Runs a filter over the output of :meth:`all` and the returns 34 | a list of cells that match that filter. 35 | """ 36 | return list(filter(fn, cls.all(interface))) 37 | 38 | 39 | cells_re = re.compile(r'Cell \d+ - ') 40 | quality_re = re.compile(r'Quality=(\d+/\d+).*Signal level=(-\d+) dBm') 41 | frequency_re = re.compile(r'([\d\.]+ .Hz).*') 42 | 43 | 44 | scalars = ( 45 | 'address', 46 | 'channel', 47 | 'mode', 48 | ) 49 | 50 | identity = lambda x: x 51 | 52 | key_translations = { 53 | 'encryption key': 'encrypted', 54 | 'essid': 'ssid', 55 | } 56 | 57 | 58 | def normalize_key(key): 59 | key = key.strip().lower() 60 | 61 | key = key_translations.get(key, key) 62 | 63 | return key.replace(' ', '') 64 | 65 | normalize_value = { 66 | 'ssid': lambda v: v.strip('"'), 67 | 'frequency': lambda v: frequency_re.search(v).group(1), 68 | 'encrypted': lambda v: v == 'on', 69 | 'channel': int, 70 | 'address': identity, 71 | 'mode': identity, 72 | } 73 | 74 | 75 | def split_on_colon(string): 76 | key, _, value = map(lambda s: s.strip(), string.partition(':')) 77 | 78 | return key, value 79 | 80 | 81 | def normalize(cell_block): 82 | """ 83 | The cell blocks come in with the every line except the first 84 | indented 20 spaces. This will remove all of that extra stuff. 85 | """ 86 | lines = textwrap.dedent(' ' * 20 + cell_block).splitlines() 87 | cell = Cell() 88 | 89 | while lines: 90 | line = lines.pop(0) 91 | 92 | if line.startswith('Quality'): 93 | cell.quality, cell.signal = quality_re.search(line).groups() 94 | elif line.startswith('Bit Rates'): 95 | values = split_on_colon(line)[1].split('; ') 96 | 97 | # consume next line of bit rates, because they are split on 98 | # different lines, sometimes... 99 | while lines[0].startswith(' ' * 10): 100 | values += lines.pop(0).strip().split('; ') 101 | 102 | cell.bitrates.extend(values) 103 | elif ':' in line: 104 | key, value = split_on_colon(line) 105 | key = normalize_key(key) 106 | 107 | if key == 'ie': 108 | if 'Unknown' in value: 109 | continue 110 | 111 | # consume remaining block 112 | values = [value] 113 | while lines and lines[0].startswith(' ' * 4): 114 | values.append(lines.pop(0).strip()) 115 | 116 | for word in values: 117 | if 'WPA2' in word: 118 | cell.encryption_type = 'wpa2-?' 119 | elif 'PSK' in word: 120 | cell.encryption_type = 'wpa2-psk' 121 | elif '802.1x' in word: 122 | cell.encryption_type = 'wpa2-eap' 123 | elif key in normalize_value: 124 | setattr(cell, key, normalize_value[key](value)) 125 | return cell 126 | -------------------------------------------------------------------------------- /wifi/scheme.py: -------------------------------------------------------------------------------- 1 | import re 2 | import itertools 3 | 4 | import wifi.subprocess_compat as subprocess 5 | import wpactrl 6 | from wifi.pbkdf2 import pbkdf2_hex 7 | 8 | 9 | def configuration(cell, passkey=None, username=None): 10 | """ 11 | Returns a dictionary of configuration options for cell 12 | 13 | Asks for a password if necessary 14 | """ 15 | if not cell.encrypted: 16 | return 17 | { 18 | 'wireless-essid': cell.ssid, 19 | 'wireless-channel': 'auto', 20 | } 21 | else: 22 | if (cell.encryption_type == 'wpa2-?') or (cell.encryption_type == 'wpa2-psk'): 23 | if len(passkey) != 64: 24 | passkey = pbkdf2_hex(passkey, cell.ssid, 4096, 32) 25 | 26 | return { 27 | 'wpa-ssid': cell.ssid, 28 | 'wpa-psk': passkey, 29 | 'wireless-channel': 'auto', 30 | } 31 | elif (cell.encryption_type == 'wpa2-eap'): 32 | return { 33 | 'ssid': '"'+cell.ssid+'"', 34 | 'key_mgmt': 'WPA-EAP', 35 | 'scan_ssid': '1', 36 | 'proto': 'RSN', 37 | 'pairwise': 'CCMP TKIP', 38 | 'group': 'CCMP TKIP', 39 | 'eap': 'PEAP', 40 | 'phase1': '"peapver=0"' 41 | 'phase2': '"MSCHAPV2"' 42 | 'identity': '"'+username+'"', 43 | 'password': '"'+passkey+'"', 44 | } 45 | else: 46 | raise NotImplementedError 47 | 48 | 49 | class Scheme(object): 50 | """ 51 | Saved configuration for connecting to a wireless network. This 52 | class provides a Python interface to the /etc/network/interfaces 53 | file. 54 | """ 55 | 56 | interfaces = '/etc/network/interfaces' 57 | 58 | def __init__(self, interface, name, options=None,encryption_type=None): 59 | self.interface = interface 60 | self.name = name 61 | self.options = options or {} 62 | self.encryption_type = encryption_type 63 | 64 | def __str__(self): 65 | """ 66 | Returns the representation of a scheme that you would need 67 | in the /etc/network/interfaces file. 68 | """ 69 | if self.encryption_type == 'wpa2-eap': 70 | iface = "auto {interface}\n".format(**vars(self)) 71 | iface += "iface {interface} inet dhcp\n".format(**vars(self)) 72 | iface += ''.join("\t pre-up wpa_supplicant -Bw -Dwext -i {interface} -c/etc/wpa_supplicant_enterprise.conf\n") 73 | iface += ''.join("\t post-down killall -q wpa_supplicant") 74 | return iface 75 | else: 76 | iface = "iface {interface} inet dhcp".format(**vars(self)) 77 | options = ''.join("\n {k} {v}".format(k=k, v=v) for k, v in self.options.items()) 78 | return iface + options + '\n' 79 | 80 | def __repr__(self): 81 | return 'Scheme(interface={interface!r}, name={name!r}, options={options!r}'.format(**vars(self)) 82 | 83 | @classmethod 84 | def all(cls): 85 | """ 86 | Returns an generator of saved schemes. 87 | """ 88 | with open(cls.interfaces, 'r') as f: 89 | return extract_schemes(f.read()) 90 | 91 | @classmethod 92 | def where(cls, fn): 93 | return list(filter(fn, cls.all())) 94 | 95 | @classmethod 96 | def find(cls, interface, name): 97 | """ 98 | Returns a :class:`Scheme` or `None` based on interface and 99 | name. 100 | """ 101 | try: 102 | return cls.where(lambda s: s.interface == interface and s.name == name)[0] 103 | except IndexError: 104 | return None 105 | 106 | @classmethod 107 | def for_cell(cls, interface, name, cell, passkey=None, username=None): 108 | """ 109 | Intuits the configuration needed for a specific 110 | :class:`Cell` and creates a :class:`Scheme` for it. 111 | """ 112 | if cell.encrypted: 113 | return cls(interface, name, configuration(cell, passkey, username), cell.encryption_type) 114 | else: 115 | return cls(interface, name, configuration(cell, passkey, username)) 116 | 117 | 118 | def save(self): 119 | """ 120 | Writes the configuration to the :attr:`interfaces` file. 121 | If WPA-EAP network, also write to /etc/wpa_supplicant_enterprise.conf 122 | """ 123 | assert not Scheme.find(self.interface, self.name) 124 | 125 | #Adapted from Ubuntu 802.1x Authentication page: https://help.ubuntu.com/community/Network802.1xAuthentication 126 | if self.encryption_type == 'wpa2-eap': 127 | configuration_file = '/etc/wpa_supplicant_enterprise.conf' 128 | with open(configuration_file, 'a') as config_file: 129 | config_file.write('ctrl_interface=/var/run/wpa_supplicant_enterprise\n') 130 | config_file.write('ctrl_interface)group=0\n') 131 | config_file.write('eapol_version=2\n') 132 | config_file.write('ap_scan=0\n') 133 | options = ''.join("\n{k}={v}".format(k=k, v=v) for k, v in test_dic.items()) 134 | config_file.write(options[1:]) 135 | else: 136 | with open(self.interfaces, 'a') as f: 137 | f.write('\n') 138 | f.write(str(self)) 139 | 140 | @property 141 | def iface(self): 142 | return '{0}-{1}'.format(self.interface, self.name) 143 | 144 | def as_args(self): 145 | args = list(itertools.chain.from_iterable( 146 | ('-o', '{k}={v}'.format(k=k, v=v)) for k, v in self.options.items())) 147 | 148 | return [self.interface + '=' + self.iface] + args 149 | 150 | def activate(self): 151 | """ 152 | Connects to the network as configured in this scheme. 153 | """ 154 | if self.encryption_type == 'wpa2-eap': 155 | response = subprocess.check_output(['wpa_supplicant', '-B', '-i', self.interface, '-Dwext', '-c', '/etc/wpa_supplicant_enterprise.conf']) 156 | if ("Authentication succeeded" in response) or ("EAP authentication completed successfully" in response): 157 | print 'Wi-Fi Connection established!' 158 | else: 159 | print 'Could not connect to Wi-Fi network!' 160 | else: 161 | subprocess.check_call(['/sbin/ifdown', self.interface]) 162 | subprocess.check_call(['/sbin/ifup'] + self.as_args()) 163 | 164 | 165 | # TODO: support other interfaces 166 | scheme_re = re.compile(r'iface\s+(?Pwlan\d?)(?:-(?P\w+))?') 167 | 168 | 169 | def extract_schemes(interfaces): 170 | lines = interfaces.splitlines() 171 | while lines: 172 | line = lines.pop(0) 173 | 174 | if line.startswith('#') or not line: 175 | continue 176 | 177 | match = scheme_re.match(line) 178 | if match: 179 | options = {} 180 | interface, scheme = match.groups() 181 | 182 | if not scheme or not interface: 183 | continue 184 | 185 | while lines and lines[0].startswith(' '): 186 | key, value = re.sub(r'\s{2,}', ' ', lines.pop(0).strip()).split(' ', 1) 187 | options[key] = value 188 | 189 | scheme = Scheme(interface, scheme, options) 190 | 191 | yield scheme 192 | -------------------------------------------------------------------------------- /wifi/subprocess_compat.py: -------------------------------------------------------------------------------- 1 | """ 2 | Python 2.6 doesn't provide subprocess.check_output or subprocess.check_call 3 | """ 4 | 5 | from subprocess import * 6 | 7 | try: 8 | check_output 9 | except NameError: 10 | # Copyright (c) 2003-2005 by Peter Astrand 11 | # 12 | # Licensed to PSF under a Contributor Agreement. 13 | # See http://www.python.org/2.4/license for licensing details. 14 | 15 | def check_output(*popenargs, **kwargs): 16 | r"""Run command with arguments and return its output as a byte string. 17 | 18 | If the exit code was non-zero it raises a CalledProcessError. The 19 | CalledProcessError object will have the return code in the returncode 20 | attribute and output in the output attribute. 21 | 22 | The arguments are the same as for the Popen constructor. Example: 23 | 24 | >>> check_output(["ls", "-l", "/dev/null"]) 25 | 'crw-rw-rw- 1 root root 1, 3 Oct 18 2007 /dev/null\n' 26 | 27 | The stdout argument is not allowed as it is used internally. 28 | To capture standard error in the result, use stderr=STDOUT. 29 | 30 | >>> check_output(["/bin/sh", "-c", 31 | ... "ls -l non_existent_file ; exit 0"], 32 | ... stderr=STDOUT) 33 | 'ls: non_existent_file: No such file or directory\n' 34 | """ 35 | if 'stdout' in kwargs: 36 | raise ValueError('stdout argument not allowed, it will be overridden.') 37 | process = Popen(stdout=PIPE, *popenargs, **kwargs) 38 | output, unused_err = process.communicate() 39 | retcode = process.poll() 40 | if retcode: 41 | cmd = kwargs.get("args") 42 | if cmd is None: 43 | cmd = popenargs[0] 44 | raise CalledProcessError(retcode, cmd, output=output) 45 | return output 46 | 47 | def check_call(*popenargs, **kwargs): 48 | """Run command with arguments. Wait for command to complete. If 49 | the exit code was zero then return, otherwise raise 50 | CalledProcessError. The CalledProcessError object will have the 51 | return code in the returncode attribute. 52 | 53 | The arguments are the same as for the Popen constructor. Example: 54 | 55 | check_call(["ls", "-l"]) 56 | """ 57 | retcode = call(*popenargs, **kwargs) 58 | if retcode: 59 | cmd = kwargs.get("args") 60 | if cmd is None: 61 | cmd = popenargs[0] 62 | raise CalledProcessError(retcode, cmd) 63 | return 0 64 | -------------------------------------------------------------------------------- /wifi/utils.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | def match(needle, haystack): 4 | """ 5 | Command-T-style string matching. 6 | """ 7 | score = 1 8 | j = 0 9 | last_match = 0 10 | needle = needle.lower() 11 | haystack = haystack.lower() 12 | 13 | for c in needle: 14 | while j < len(haystack) and haystack[j] != c: 15 | j += 1 16 | if j >= len(haystack): 17 | return 0 18 | score += 1 / (last_match + 1.) 19 | last_match = j 20 | j += 1 21 | return score 22 | 23 | 24 | def print_table(matrix): 25 | """ 26 | Prints a left-aligned table of elements. 27 | """ 28 | lengths = [max(map(len, column)) for column in zip(*matrix)] 29 | format = ' '.join('{{{0}:<{1}}}'.format(i, length) for i, length in enumerate(lengths)) 30 | 31 | for row in matrix: 32 | print(format.format(*row)) 33 | --------------------------------------------------------------------------------