├── MANIFEST.in ├── setup.cfg ├── CHANGES ├── __init__.py ├── TODO ├── captiv8 ├── __init__.py ├── collect.py └── captiv.py ├── LICENSE ├── README.md ├── .gitignore └── setup.py /MANIFEST.in: -------------------------------------------------------------------------------- 1 | # setup paramaters for captiv8 via PyPi/pip install 2 | include LICENSE CHANGES README.md -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | # pip installation and distribution sudo pip install captiv8 2 | 3 | [bdist_wheel] 4 | universal=1 5 | 6 | [metadata] 7 | description-file = README.md 8 | -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | captiv8 CHANGES 2 | 3 | v 0.0.1 Proof of Concept 4 | o basic UI implemented 5 | v 0.0.2 6 | o scanning portion complete (needs a little more tinkering) 7 | 8 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # captiv8 root Distribution directory 2 | # Do not import from this directory 3 | # To install 4 | # use pip ('sudo pip install captiv8') 5 | # or download latest tarbal and untar. 6 | # execute either sudo python capitv8.py or run from the captiv8/captiv8 directory -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | 2. Flash (and beep) does not work 2 | o could we reverse highlight the ssid instead? 3 | 3. add arrow key (left, right) support to configure->SSID 4 | 4. In configure, would like to strikethrough dev name if it cannot be used 5 | tried overwriting with '-' but the background covers it up 6 | 6. configure (errwin) is a hodgepodge of coordinate systems, relative and absolute 7 | but it seems to be the only way to get it to work. Determining the mouse click 8 | point seems to be based on the main window's coord system, but everything else 9 | is based on the new window's coord systemq 10 | 7. have set the execution loop in configure to catch curses errors on curses.getmouse 11 | due to occasional "getmouse() returned err". don't know what this is from. 12 | For now, I catch and continue but this may end up be some unrecoverable error 13 | and this solution will cause the app to hang. 14 | 8. in configure do we leave the return as is on Set even if nothing was changed? 15 | o this could be used to delete/reset config 16 | o but is user friendly? 17 | 9. checking x length on radio options is pointless 18 | 11. Add a info button for chip hw, i.e. driver, chipset, manufacturer and 19 | other stuff? 20 | 12. Handle shrink/grow 21 | o when shrunk beyond 80x24 and then "regrown", screen does not regenerate/redraw 22 | 14. add a clear btn on configure 23 | o harder than it's worth now with everything already present 24 | 18. the color scheme for state symbol is hard to see (may have to do all green) 25 | or green for connected, white for everything else 26 | 19. add a wait window for stopping scanning and quitting while scanning is active 27 | 21. Pull off STAs that have reassociated within another ESSID 28 | 24. Have te reset the info window status on stopped? 29 | 25. After running, have to recheck if configure is selected and reset aps, stas 30 | unless same SSID is being used 31 | 27. Have to handle the connection to read/display errors 32 | 29. add a reset button to configure window 33 | 30. need better editing capability for ssid in configure window 34 | 32. On view, add number next to bssid i.e. 1., 2., etc. 35 | 33. When running, if you hit run again, message says "cannot run, not configured" 36 | 34. When stopped after running, pressing run again results in 37 | Traceback (most recent call last): 38 | File "captiv.py", line 1057, in 39 | for sta in nets['stas']: 40 | KeyError: 'stas' 41 | -------------------------------------------------------------------------------- /captiv8/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ captiv8 Captive Portal Evasion Tool 3 | 4 | Copyright (C) 2016 Dale V. Patterson (wraith.wireless@yandex.com) 5 | 6 | This program is free software: you can redistribute it and/or modify it under 7 | the terms of the GNU General Public License as published by the Free Software 8 | Foundation, either version 3 of the License, or (at your option) any later 9 | version. 10 | 11 | Redistribution and use in source and binary forms, with or without modifications, 12 | are permitted provided that the following conditions are met: 13 | o Redistributions of source code must retain the above copyright notice, this 14 | list of conditions and the following disclaimer. 15 | o Redistributions in binary form must reproduce the above copyright notice, 16 | this list of conditions and the following disclaimer in the documentation 17 | and/or other materials provided with the distribution. 18 | o Neither the name of the orginal author Dale V. Patterson nor the names of any 19 | contributors may be used to endorse or promote products derived from this 20 | software without specific prior written permission. 21 | 22 | Requires: 23 | linux (3.x or 4.x kernel) 24 | Python 2.7 25 | PyRIC >= 0.1.5 26 | scapy >= 2.2.0 27 | 28 | captiv8 0.0.1 29 | desc: Captiv Portal Evasion Tool 30 | includes: cap.py 31 | changes: 32 | See CHANGES in top-level directory 33 | 34 | WARNING: DO NOT import * 35 | 36 | """ 37 | 38 | __name__ = 'captiv8' 39 | __license__ = 'GPLv3' 40 | __version__ = '0.0.3' 41 | __date__ = 'September 2016' 42 | __author__ = 'Dale Patterson' 43 | __maintainer__ = 'Dale Patterson' 44 | __email__ = 'wraith.wireless@yandex.com' 45 | __status__ = 'Development' 46 | 47 | # define captiv8 exceptions 48 | # all exceptions are tuples t=(error code,error message) 49 | class error(EnvironmentError): pass 50 | 51 | # for use in setup.py 52 | 53 | # redefine version for easier access 54 | version = __version__ 55 | 56 | # define long description 57 | long_desc = """ 58 | captiv8 is a (Linux only) captive portal evasion tool. It enumerates all BSSIDs 59 | that are part of a specified SSID and identifies STAs (clients) to masquerade as. 60 | While this could be used nefariously, IOT evade paying a subscription fee, the 61 | primary purpose and intent is to build a tool that allows for some anonymity on 62 | open networks and as a tool to test your captive portal or guest networks. 63 | 64 | In short, captiv8 is an evil twin on the STA side. 65 | """ 66 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | captiv8: Captive Portal Evasion 2 | 3 | Copyright (C) 2016 Dale V. Patterson (wraith.wireless@yandex.com) 4 | 5 | This program is free software: you can redistribute it and/or modify it under 6 | the terms of the GNU General Public License as published by the Free Software 7 | Foundation, either version 3 of the License, or (at your option) any later 8 | version. 9 | 10 | Redistribution and use in source and binary forms, with or without modifications, 11 | are permitted provided that the following conditions are met: 12 | o Redistributions of source code must retain the above copyright notice, this 13 | list of conditions and the following disclaimer. 14 | o Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | o Neither the name of the orginal author Dale V. Patterson nor the names of any 18 | contributors may be used to endorse or promote products derived from this 19 | software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS" AND 22 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 23 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 24 | IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 25 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT 26 | OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 27 | INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, 28 | STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 29 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | 31 | captiv8 is free software but use, duplication or disclosure by the United States 32 | Government is subject to the restrictions set forth in DFARS 252.227-7014. 33 | 34 | Use of this software is governed by all applicable federal, state and local 35 | laws of the United States and subject to the laws of the country where you reside. 36 | The copyright owner and contributors will be not be held liable for use of this 37 | software in furtherance of or with intent to commit any fraudulent or other illegal 38 | activities, or otherwise in violation of any applicable law, regulation or legal 39 | agreement. 40 | 41 | See for a copy of the GNU General Public License. 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CAPTIV8 0.0.1: Captive Portal Evasion 2 | ___ _____ _____ _______ _______ _ _ _____ 3 | _(___)_ (_____) (_____)(__ _ __)(_______)(_) (_) (_____) 4 | (_) (_)(_)___(_)(_)__(_) (_) (_) (_) (_)(_)___(_) 5 | (_) _ (_______)(_____) (_) (_) (_) (_) (_____) 6 | (_)___(_)(_) (_)(_) (_) __(_)__ (_)_(_) (_)___(_) 7 | (___) (_) (_)(_) (_) (_______) (___) (_____) 8 | ## The Evil Twin's Younger Brother 9 | 10 | [![License: GPLv3](https://img.shields.io/pypi/l/captiv8.svg)](https://github.com/wraith-wireless/captiv8/blob/master/LICENSE) 11 | [![PyPI Version](https://img.shields.io/pypi/v/captiv8.svg)](https://pypi.python.org/pypi/captiv8) 12 | ![Supported Python Versions](https://img.shields.io/pypi/pyversions/captiv8.svg) 13 | ![Software status](https://img.shields.io/pypi/status/captiv8.svg) 14 | 15 | ## 1 DESCRIPTION: 16 | captiv8 is a captive portal evasion tool. It enumerates all BSSIDs that are part 17 | of a specified SSID (or ESSID) and identifies potential STAs (clients) to 18 | masquerade as. 19 | 20 | captiv8 can be used for a variety of reasons: 21 | 22 | 1. You're an asshole and you want to surf for free off of someone else's dime whether 23 | it's a paid subscription hotspot or a guest network in your neighborhood with 24 | whitelisted MACs 25 | 2. You want some anonymity. Of course you could just switch your mac address but 26 | your traffic is tied to the same MAC. 27 | * With captiv8, your traffic will blend in with someone else's 28 | * It's a free hotspot but requires some PII to pass the captive portal 29 | i.e. Last name and room number 30 | 3. You're a legimate hotspot owner or guest network owner and want to test your 31 | network/ability to identify illegitimate access. 32 | 33 | ## 2. INSTALLING/USING: 34 | 35 | ### a. Requirements 36 | captiv8 requires a Linux box preferred kernel 3.13.x and greater and Python 2.7. 37 | It also requires the packages Scapy and PyRIC. You'll also need a wireless card 38 | that supports monitor mode and nl80211. And of course, an open network to test. 39 | 40 | ### b. Install from Package Manager 41 | Obviously, the easiest way to install captiv8 is through PyPI: 42 | 43 | ```bash 44 | > sudo pip install captiv8 45 | ``` 46 | 47 | ### c. Install from Source 48 | Download the captiv8 tarball, untar to favorate directory and execute 49 | 50 | ```bash 51 | sudo python captiv.py 52 | ``` 53 | 54 | from the captiv8/captiv8 directory. 55 | 56 | Scapy should be present on most systems and PyRIC can be installed via pip. 57 | 58 | ## 3. USING 59 | ATT terminal must be 80x24 before executing captiv8. 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # modified from https://github.com/github/gitignore/blob/master/Python.gitignore 2 | #Copyright (c) 2016 GitHub, Inc. 3 | # 4 | #Permission is hereby granted, free of charge, to any person obtaining a 5 | #copy of this software and associated documentation files (the "Software"), 6 | #to deal in the Software without restriction, including without limitation 7 | #the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | #and/or sell copies of the Software, and to permit persons to whom the 9 | #Software is furnished to do so, subject to the following conditions: 10 | # 11 | #The above copyright notice and this permission notice shall be included in 12 | #all copies or substantial portions of the Software. 13 | # 14 | #THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | #IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | #FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | #AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | #LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | #FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | # 21 | #DEALINGS IN THE SOFTWARE. 22 | 23 | # Byte-compiled / optimized / DLL files 24 | __pycache__/ 25 | *.py[cod] 26 | *$py.class 27 | 28 | # C extensions 29 | *.so 30 | 31 | # Distribution / packaging 32 | .Python 33 | env/ 34 | build/ 35 | develop-eggs/ 36 | dist/ 37 | downloads/ 38 | eggs/ 39 | .eggs/ 40 | lib 41 | lib64/ 42 | parts/ 43 | sdist/ 44 | var/ 45 | *.egg-info/ 46 | .installed.cfg 47 | *.egg 48 | 49 | # PyCharm 50 | .idea 51 | .git 52 | 53 | # PyInstaller 54 | # Usually these files are written by a python script from a template 55 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 56 | *.manifest 57 | *.spec 58 | 59 | # Installer logs 60 | pip-log.txt 61 | pip-delete-this-directory.txt 62 | 63 | # Unit test / coverage reports 64 | htmlcov/ 65 | .tox/ 66 | .coverage 67 | .coverage.* 68 | .cache 69 | nosetests.xml 70 | coverage.xml 71 | *,cover 72 | .hypothesis/ 73 | 74 | # Translations 75 | *.mo 76 | *.pot 77 | 78 | # Django stuff: 79 | *.log 80 | local_settings.py 81 | 82 | # Flask stuff: 83 | instance/ 84 | .webassets-cache 85 | 86 | # Scrapy stuff: 87 | .scrapy 88 | 89 | # Sphinx documentation 90 | docs/_build/ 91 | 92 | # PyBuilder 93 | target/ 94 | 95 | # IPython Notebook 96 | .ipynb_checkpoints 97 | 98 | # pyenv 99 | .python-version 100 | 101 | # celery beat schedule file 102 | celerybeat-schedule 103 | 104 | # dotenv 105 | .env 106 | 107 | # virtualenv 108 | venv/ 109 | ENV/ 110 | 111 | # Spyder project settings 112 | .spyderproject 113 | 114 | # Rope project settings 115 | .ropeproject 116 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ setup.py: install captiv8 4 | 5 | Copyright (C) 2016 Dale V. Patterson (wraith.wireless@yandex.com) 6 | 7 | This program is free software: you can redistribute it and/or modify it under 8 | the terms of the GNU General Public License as published by the Free Software 9 | Foundation, either version 3 of the License, or (at your option) any later 10 | version. 11 | 12 | Redistribution and use in source and binary forms, with or without modifications, 13 | are permitted provided that the following conditions are met: 14 | o Redistributions of source code must retain the above copyright notice, this 15 | list of conditions and the following disclaimer. 16 | o Redistributions in binary form must reproduce the above copyright notice, 17 | this list of conditions and the following disclaimer in the documentation 18 | and/or other materials provided with the distribution. 19 | o Neither the name of the orginal author Dale V. Patterson nor the names of any 20 | contributors may be used to endorse or promote products derived from this 21 | software without specific prior written permission. 22 | 23 | sudo pip install captiv8 24 | 25 | """ 26 | 27 | #__name__ = 'setup' 28 | __license__ = 'GPLv3' 29 | __version__ = '0.0.1' 30 | __date__ = 'July 2016' 31 | __author__ = 'Dale Patterson' 32 | __maintainer__ = 'Dale Patterson' 33 | __email__ = 'wraith.wireless@yandex.com' 34 | __status__ = 'Production' 35 | 36 | from setuptools import setup, find_packages 37 | import captiv8 38 | 39 | setup(name='captiv8', 40 | version=captiv8.version, 41 | description="Captive Portal Evasion Tool", 42 | long_description=captiv8.long_desc, 43 | url='http://wraith-wireless.github.io/captiv8/', 44 | download_url="https://github.com/wraith-wireless/captiv8/archive/"+captiv8.version+".tar.gz", 45 | author=captiv8.__author__, 46 | author_email=captiv8.__email__, 47 | maintainer=captiv8.__maintainer__, 48 | maintainer_email=captiv8.__email__, 49 | license=captiv8.__license__, 50 | classifiers=['Development Status :: 4 - Beta', 51 | 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 52 | 'Intended Audience :: Developers', 53 | 'Intended Audience :: System Administrators', 54 | 'Topic :: Security', 55 | 'Topic :: Software Development', 56 | 'Topic :: Security', 57 | 'Topic :: System :: Networking', 58 | 'Topic :: Utilities', 59 | 'Operating System :: POSIX :: Linux', 60 | 'Programming Language :: Python', 61 | 'Programming Language :: Python :: 2.7'], 62 | keywords='Linux Python pentest hacking wireless WLAN WiFi 802.11', 63 | packages=find_packages(), 64 | install_requires = ['PyRIC','itamae'] 65 | # TODO: add dependencies 66 | ) 67 | -------------------------------------------------------------------------------- /captiv8/collect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ collect.py: Collect/Inspect packets 4 | 5 | Copyright (C) 2016 Dale V. Patterson (wraith.wireless@yandex.com) 6 | 7 | This program is free software: you can redistribute it and/or modify it under 8 | the terms of the GNU General Public License as published by the Free Software 9 | Foundation, either version 3 of the License, or (at your option) any later 10 | version. 11 | 12 | Redistribution and use in source and binary forms, with or without 13 | modifications, are permitted provided that the following conditions are met: 14 | o Redistributions of source code must retain the above copyright notice, this 15 | list of conditions and the following disclaimer. 16 | o Redistributions in binary form must reproduce the above copyright notice, 17 | this list of conditions and the following disclaimer in the documentation 18 | and/or other materials provided with the distribution. 19 | o Neither the name of the orginal author Dale V. Patterson nor the names of 20 | any contributors may be used to endorse or promote products derived from 21 | this software without specific prior written permission. 22 | 23 | Provides the Collector and Tuner class 24 | 25 | """ 26 | 27 | __name__ = 'collect' 28 | __license__ = 'GPLv3' 29 | __version__ = '0.0.2' 30 | __date__ = 'August 2016' 31 | __author__ = 'Dale Patterson' 32 | __maintainer__ = 'Dale Patterson' 33 | __email__ = 'wraith.wireless@yandex.com' 34 | __status__ = 'Development' 35 | 36 | import multiprocessing as mp 37 | import threading 38 | import select 39 | import socket 40 | import time 41 | import pyric 42 | import pyric.pyw as pyw 43 | import pyric.lib.libnl as nl 44 | import pyric.utils.channels as channels 45 | import itamae.radiotap as rtap 46 | import itamae.mpdu as mpdu 47 | 48 | SCAN = 0.2 49 | 50 | class Tuner(threading.Thread): 51 | """ tunes radio """ 52 | def __init__(self,card,scan): 53 | """ 54 | tunes card 55 | :param card: the card/radio to tune 56 | :param scan: scan list of available channels, a list of tuples 57 | t = (rf, channel width) 58 | """ 59 | threading.Thread.__init__(self) 60 | self._card = card 61 | self._scan = scan 62 | 63 | def run(self): 64 | # we'll pull out scan & card to avoid calling it every SCAN seconds 65 | i = 0 66 | card = self._card 67 | scan = self._scan 68 | n = len(self._scan) 69 | 70 | # loop until the card is destroyed. we'll use pyric.error as 71 | # a poison pill 72 | nlsock = nl.nl_socket_alloc() 73 | while True: 74 | try: 75 | time.sleep(SCAN) 76 | i = (i + 1) % n 77 | pyw.freqset(card, scan[i][0], scan[i][1], nlsock) 78 | except pyric.error: 79 | # ideally we should check below and return error if 80 | # we didn't lose the card 81 | #if not pyw.validcard(card,nlsock): break 82 | break 83 | nl.nl_socket_free(nlsock) 84 | 85 | class Sniffer(threading.Thread): 86 | """ sniffs packets """ 87 | def __init__(self,pq,dev): 88 | """ 89 | initialize sniffer 90 | :param pq: packet queue 91 | :param dev: the interface to sniff packets from 92 | """ 93 | threading.Thread.__init__(self) 94 | self._pq = pq 95 | self._s = None 96 | self._setup(dev) 97 | 98 | def run(self): 99 | s = self._s 100 | q = self._pq 101 | try: 102 | while True: 103 | frame = s.recv(7935) 104 | q.put(frame) 105 | except socket.error: # assume any socket error means the Card is closed 106 | self._teardown() 107 | 108 | def _setup(self,dev): 109 | try: 110 | self._s = socket.socket(socket.AF_PACKET, 111 | socket.SOCK_RAW, 112 | socket.htons(0x0003)) 113 | self._s.bind((dev,0x0003)) 114 | except socket.error as e: 115 | raise RuntimeError(e) 116 | 117 | def _teardown(self): 118 | if self._s: self._s.close() 119 | self._s = None 120 | 121 | # noinspection PyCallByClass 122 | class Collector(mp.Process): 123 | """ Collects data on wireless nets """ 124 | def __init__(self,conn,dq,ssid,dev,aps,stas): 125 | """ 126 | initialize Collector 127 | :param conn: pipe connection 128 | :param dq: data queue 129 | :param ssid: Name of ssid to collect on 130 | :param dev: device to use for collection 131 | :param aps: AP dict 132 | :param stas: STA dict 133 | """ 134 | mp.Process.__init__(self) 135 | self._ssid = ssid # ssid to scan for 136 | self._dev = dev # device name 137 | self._conn = conn # connecion to captiv 138 | self._aps = aps # AP dict 139 | self._stas = stas # STA dict 140 | self._datq = dq # data queue 141 | self._pktq = None # packet queue 142 | self._tuner = None # tuner thread 143 | self._sniffer = None # the sniffer thread 144 | self._err = None # err message 145 | self._dinfo = None # dev's original info 146 | self._card = None # the card to use 147 | self._setup() 148 | 149 | def terminate(self): 150 | """ override terminate """ 151 | self._teardown() 152 | mp.Process.terminate(self) 153 | 154 | def run(self): 155 | """ execution loop """ 156 | # start our threads 157 | self._tuner.start() 158 | self._sniffer.start() 159 | 160 | # set up our inputs list for select & stop token 161 | # noinspection PyProtectedMember 162 | ins = [self._conn,self._pktq._reader] 163 | stop = False 164 | 165 | while not stop: 166 | try: 167 | rs,_,_ = select.select(ins,[],[],1) 168 | for r in rs: 169 | if r == self._conn: 170 | tkn = self._conn.recv() 171 | if tkn == '!QUIT!': stop = True 172 | else: 173 | self._processpkt() 174 | #except Exception as e: 175 | # # catchall 176 | # self._conn.send(('!ERR',"{0} {1}".format(type(e).__name__,e))) 177 | # stop = True 178 | except select.error as e: 179 | if e[0] != 4: 180 | self._conn.send(('!ERR',"{0} {1}".format(type(e).__name__,e))) 181 | stop = True 182 | if not self._teardown(): 183 | self._conn.send(('!ERR!',self._err)) 184 | 185 | def _processpkt(self): 186 | """ 187 | pulls a packet of the queue and processes it. Sends notification of 188 | new APs found and notifications of new STAs or STAs with updated 189 | timestamps 190 | """ 191 | # get the packet off the queue, parse it. Return if no Dot11 192 | pkt = self._pktq.get() 193 | dR = rtap.parse(pkt) 194 | dM = mpdu.parse(pkt[dR.sz:],'fcs' in dR.flags) 195 | if dM.error: return 196 | 197 | if dM.type == 0: # mgmt 198 | # for BSSIDs, look at beacon, assoc request, and probe response 199 | if dM.subtype == 8 or dM.subtype == 0 or dM.subtype == 5: 200 | ssids, = dM.getie([mpdu.EID_SSID]) 201 | if self._ssid in ssids: 202 | if not dM.addr3 in self._aps: 203 | self._aps[dM.addr3] = dR.rss 204 | self._datq.put(('!AP-new!',(dM.addr3,self._aps[dM.addr3]))) 205 | else: 206 | self._aps[dM.addr3] = dR.rss 207 | self._datq.put(('!AP-upd!',(dM.addr3,self._aps[dM.addr3]))) 208 | elif dM.type == 2: # data 209 | rss = None 210 | if dM.flags['td'] and not dM.flags['fd']: 211 | bssid = dM.addr1 212 | sta = dM.addr2 213 | rss = dR.rss 214 | elif not dM.flags['td'] and dM.flags['fd']: 215 | bssid = dM.addr2 216 | sta = dM.addr1 217 | else: 218 | return 219 | 220 | # do nothing if we got a broadcast 221 | if sta == "ff:ff:ff:ff:ff:ff": return 222 | 223 | # if we have a matching bssid proceed 224 | if bssid in self._aps: 225 | if not sta in self._stas: 226 | self._stas[sta] = {'ASW':bssid, 227 | 'ts':time.time(), 228 | 'rf':dR.channel, 229 | 'rss':rss} 230 | self._datq.put(('!STA-new!',(sta,self._stas[sta]))) 231 | else: 232 | self._stas[sta]['ts'] = time.time() 233 | self._stas[sta]['rf'] = dR.channel 234 | if rss: self._stas[sta]['rss'] = rss 235 | self._datq.put(('!STA-upd!',(sta,self._stas[sta]))) 236 | 237 | def _setup(self): 238 | """ setup radio and tuning thread """ 239 | # set up the radio for collection 240 | nlsock = None 241 | try: 242 | # get a netlink socket for this 243 | nlsock = nl.nl_socket_alloc() 244 | 245 | # get dev info for dev and it's phy index 246 | self._dinfo = pyw.devinfo(self._dev,nlsock) 247 | phy = self._dinfo['card'].phy 248 | 249 | # delete all associated interfaces 250 | for c,_ in pyw.ifaces(self._dinfo['card'],nlsock): pyw.devdel(c,nlsock) 251 | 252 | # create a new card in monitor mode 253 | self._card = pyw.phyadd(phy,'cap8','monitor',None,nlsock) 254 | pyw.up(self._card) 255 | 256 | # determine scannable channels, then go to first channel 257 | scan = [] 258 | for rf in pyw.devfreqs(self._card,nlsock): 259 | for chw in channels.CHTYPES: 260 | try: 261 | pyw.freqset(self._card,rf,chw,nlsock) 262 | scan.append((rf, chw)) 263 | except pyric.error as e: 264 | if e.errno != pyric.EINVAL: raise 265 | assert scan 266 | pyw.freqset(self._card,scan[0][0],scan[0][1],nlsock) 267 | 268 | # create the tuner & sniffer 269 | self._pktq = mp.Queue() 270 | self._tuner = Tuner(self._card,scan) 271 | self._sniffer = Sniffer(self._pktq,self._card.dev) 272 | except RuntimeError as e: 273 | self._teardown() 274 | raise RuntimeError("Error binding socket {0}".format(e)) 275 | except threading.ThreadError as e: 276 | self._teardown() 277 | raise RuntimeError("Unexepected error in the workers {0}".format(e)) 278 | except AssertionError: 279 | self._teardown() 280 | raise RuntimeError("No valid scan channels found") 281 | except nl.error as e: 282 | self._teardown() 283 | raise RuntimeError("ERRNO {0} {1}".format(e.errno, e.strerror)) 284 | except pyric.error as e: 285 | self._teardown() # attempt to restore 286 | raise RuntimeError("ERRNO {0} {1}".format(e.errno, e.strerror)) 287 | finally: 288 | nl.nl_socket_free(nlsock) 289 | 290 | def _teardown(self): 291 | """ restore radio and wait on tuning thread""" 292 | clean = True 293 | self._err = "" 294 | 295 | # restore the radio - this will have the side effect of 296 | # causing the threads to error out and quit 297 | try: 298 | if self._card: 299 | phy = self._card.phy 300 | pyw.devdel(self._card) 301 | card = pyw.phyadd(phy,self._dev,self._dinfo['mode']) 302 | pyw.up(card) 303 | except pyric.error as e: 304 | clean = False 305 | self._err = "ERRNO {0} {1}".format(e.errno, e.strerror) 306 | 307 | # join threads, waiting a short time before continuing 308 | try: 309 | self._tuner.join(5.0) 310 | except (AttributeError,RuntimeError): 311 | # either tuner is None, or it never started 312 | pass 313 | 314 | try: 315 | self._sniffer.join(5.0) 316 | except (AttributeError, RuntimeError): 317 | # either sniffer is None, or it never started 318 | pass 319 | 320 | if threading.active_count() > 0: 321 | clean = False 322 | self._err += "One or more workers failed to stop" 323 | 324 | return clean -------------------------------------------------------------------------------- /captiv8/captiv.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | """ capitv.py: Captive Portal Evasion Tool interface 4 | 5 | Copyright (C) 2016 Dale V. Patterson (wraith.wireless@yandex.com) 6 | 7 | This program is free software: you can redistribute it and/or modify it under 8 | the terms of the GNU General Public License as published by the Free Software 9 | Foundation, either version 3 of the License, or (at your option) any later 10 | version. 11 | 12 | Redistribution and use in source and binary forms, with or without 13 | modifications, are permitted provided that the following conditions are met: 14 | o Redistributions of source code must retain the above copyright notice, this 15 | list of conditions and the following disclaimer. 16 | o Redistributions in binary form must reproduce the above copyright notice, 17 | this list of conditions and the following disclaimer in the documentation 18 | and/or other materials provided with the distribution. 19 | o Neither the name of the orginal author Dale V. Patterson nor the names of 20 | any contributors may be used to endorse or promote products derived from 21 | this software without specific prior written permission. 22 | 23 | Provides the user interface to captiv8 24 | """ 25 | 26 | #__name__ = 'capitv' 27 | __license__ = 'GPLv3' 28 | __version__ = '0.0.1' 29 | __date__ = 'July 2016' 30 | __author__ = 'Dale Patterson' 31 | __maintainer__ = 'Dale Patterson' 32 | __email__ = 'wraith.wireless@yandex.com' 33 | __status__ = 'Development' 34 | 35 | import curses 36 | import curses.ascii as ascii 37 | import multiprocessing as mp 38 | from Queue import Empty 39 | import threading 40 | import time 41 | import os, sys 42 | import pyric 43 | import pyric.pyw as pyw 44 | from pyric.utils.channels import rf2ch,ch2rf 45 | import pyric.net.if_h as ifh 46 | import captiv8 47 | import captiv8.collect as collect 48 | 49 | #### CONSTANTS 50 | 51 | # MAIN WINDOW BANNER 52 | _BANNER_ = [ 53 | " ___ _____ _____ _______ _______ _ _ _____", 54 | " _(___)_ (_____) (_____)(__ _ __)(_______)(_) (_) (_____)", 55 | "(_) (_)(_)___(_)(_)__(_) (_) (_) (_) (_)(_)___(_)", 56 | "(_) _ (_______)(_____) (_) (_) (_) (_) (_____)", 57 | "(_)___(_)(_) (_)(_) (_) __(_)__ (_)_(_) (_)___(_)", 58 | " (___) (_) (_)(_) (_) (_______) (___) (_____)", 59 | ] 60 | 61 | # PROGRAM STATE DEFINITIONS 62 | _INVALID_ = 0 63 | _CONFIGURED_ = 1 64 | _SCANNING_ = 2 65 | _STOPPED_ = 3 66 | _CONNECTING_ = 4 67 | _CONNECTED_ = 5 68 | _GETTINGIP_ = 6 69 | _VERIFYING_ = 7 70 | _OPERATIONAL_ = 8 71 | _QUITTING_ = 9 72 | _STATE_FLAG_NAMES_ = [ 73 | 'invalid ', 74 | 'configured ', 75 | 'scanning ', 76 | 'stopped ', 77 | 'connecting ', 78 | 'connected ', 79 | 'gettingip ', 80 | 'verifying ', 81 | 'operational', 82 | 'quitting '] 83 | 84 | # FIXED LENGTHS 85 | _IPLEN_ = 15 86 | _DEVLEN_ = ifh.IFNAMSIZ 87 | _MACLEN_ = 17 88 | _SSIDLEN_ = 32 89 | _FIXLEN_ = 10 # arbitrary fixed field length of 10 90 | 91 | # COLORS & COLOR PAIRS 92 | COLORS = 14 93 | BLACK,RED,GREEN,YELLOW,BLUE,MAGENTA,CYAN,WHITE,GRAY,BUTTON,HLITE,ERR,WARN,NOTE = range(COLORS) 94 | CPS = [None] * COLORS 95 | 96 | #### errors 97 | class error(EnvironmentError): pass 98 | 99 | #### status update thread 100 | 101 | class InfoUpdateThread(threading.Thread): 102 | """ updates Info Window status symbol/statement """ 103 | def __init__(self,win,block,state,iws): 104 | """ 105 | initialize thread 106 | :param win: the info window 107 | :param block: blocking event 108 | :param state: state dictionary 109 | :param iws: the info window output dict should have the following keys: 110 | 'current', 'current-msg' 111 | """ 112 | threading.Thread.__init__(self) 113 | self._win = win 114 | self._hold = block 115 | self._state = state 116 | self._iws = iws 117 | 118 | def run(self): 119 | """ write current state symbol and info """ 120 | i = 0 121 | symbol = color = None 122 | 123 | # loop until it's time to exit 124 | while True: 125 | self._hold.wait() 126 | s = self._state['state'] 127 | if s == _QUITTING_: return 128 | if s == _INVALID_ or s == _CONFIGURED_: 129 | color = CPS[RED] 130 | symbol = '?' if s == _INVALID_ else '-' 131 | elif s == _OPERATIONAL_: 132 | color = CPS[GREEN] 133 | symbol = '+' 134 | elif s == _QUITTING_: 135 | color = CPS[GREEN] 136 | symbol = '#' 137 | elif s > _CONFIGURED_: 138 | color = CPS[YELLOW] 139 | symbol = '/' if i else '\\' 140 | i = (i+1) % 2 141 | self._win.addch(self._iws['current'][0], 142 | self._iws["current"][1], 143 | symbol,color) 144 | self._win.addstr(self._iws['current-msg'][0], 145 | self._iws['current-msg'][1], 146 | _STATE_FLAG_NAMES_[s],CPS[WHITE]) 147 | self._win.refresh() 148 | time.sleep(0.1) 149 | 150 | class DataUpdateThread(threading.Thread): 151 | """ updates Data dicts and message """ 152 | def __init__(self,win,block,dq,nets,state,iws): 153 | """ 154 | initialize thread 155 | :param win: the info window 156 | :param block: blocking event 157 | :param dq: data queue 158 | :param nets: network dict 159 | :param state: state dict 160 | :param iws: the info window output dict should have the following keys: 161 | 'data-msg' 162 | """ 163 | threading.Thread.__init__(self) 164 | self._win = win 165 | self._hold = block 166 | self._dq = dq 167 | self._nets = nets 168 | self._state = state 169 | self._iws = iws 170 | 171 | def run(self): 172 | # loop until it's time to exit 173 | while True: 174 | self._hold.wait() 175 | s = self._state['state'] 176 | if s == _QUITTING_: return 177 | while True: 178 | try: 179 | # get the next message and clear the data message field 180 | tkn,data = self._dq.get_nowait() 181 | self._win.addstr(self._iws['data-msg'][0], 182 | self._iws['data-msg'][1], 183 | ' '*self._iws['data-msg'][2], 184 | CPS[WHITE]) 185 | 186 | # process the token 187 | if tkn == '!AP-new!': 188 | bssid,rss = data 189 | self._nets[bssid] = {'ch':None,'rss':rss,'stas':{}} 190 | msg = "Found AP w/ BSSID {0}. Total = {1}" 191 | msg = msg.format(bssid,len(self._nets)) 192 | self._win.addstr(self._iws['data-msg'][0], 193 | self._iws['data-msg'][1], 194 | msg,CPS[WHITE]) 195 | elif tkn == '!AP-upd!': 196 | bssid,rss = data 197 | self._nets[bssid]['rss'] = rss 198 | elif tkn == '!STA-new!': 199 | sta,sinfo = data 200 | bssid = sinfo['ASW'] 201 | self._nets[bssid]['stas'][sta] = { 202 | 'ts':sinfo['ts'], 203 | 'rss':sinfo['rss'], 204 | 'spoofed':0, 205 | 'success':0, 206 | } 207 | self._nets[bssid]['ch'] = rf2ch(sinfo['rf']) 208 | msg = "Found STA {0} ASW BSSID {1}".format(sta,sinfo['ASW']) 209 | self._win.addstr(self._iws['data-msg'][0], 210 | self._iws['data-msg'][1], 211 | msg,CPS[WHITE]) 212 | elif tkn == '!STA-upd!': 213 | sta,sinfo = data 214 | bssid = sinfo['ASW'] 215 | self._nets[bssid]['stas'][sta]['ts'] = sinfo['ts'] 216 | self._nets[bssid]['stas'][sta]['rss'] = sinfo['rss'] 217 | self._nets[bssid]['ch'] = rf2ch(sinfo['rf']) 218 | self._win.refresh() 219 | except Empty: 220 | time.sleep(0.5) 221 | break 222 | 223 | #### INIT/DEINIT 224 | 225 | def setup(): 226 | """ 227 | sets environment up and creates main window 228 | :returns: the main window object 229 | """ 230 | # setup the console 231 | mmask = curses.ALL_MOUSE_EVENTS # for now accept all mouse events 232 | main = curses.initscr() # get a window object 233 | y,x = main.getmaxyx() # get size 234 | if y < 24 or x < 80: # verify minimum size rqmts 235 | raise RuntimeError("Terminal must be at least 80 x 24") 236 | curses.noecho() # turn off key echoing 237 | curses.cbreak() # turn off key buffering 238 | curses.mousemask(mmask) # accept mouse events 239 | initcolors() # turn on and set color pallet 240 | main.keypad(1) # let curses handle multibyte special keys 241 | main.clear() # erase everything 242 | banner(main) # write the banner 243 | mainmenu(main) # then the min and menu 244 | main.attron(CPS[RED]) # make the border red 245 | main.border(0) # place the border 246 | main.attroff(CPS[RED]) # turn off the red 247 | curses.curs_set(0) # hide the cursor 248 | main.refresh() # and show everything 249 | return main 250 | 251 | def teardown(win): 252 | """ 253 | returns console to normal state 254 | :param win: the window 255 | """ 256 | # tear down the console 257 | curses.nocbreak() 258 | if win: win.keypad(0) 259 | curses.echo() 260 | curses.endwin() 261 | 262 | def initcolors(): 263 | """ initialize color pallet """ 264 | curses.start_color() 265 | if not curses.has_colors(): 266 | raise RuntimeError("Sorry. Terminal does not support colors") 267 | 268 | # setup colors on black background 269 | for i in range(1,9): 270 | curses.init_pair(i,i,BLACK) 271 | CPS[i] = curses.color_pair(i) 272 | 273 | # have to individually set up special cases 274 | curses.init_pair(BUTTON,WHITE,GRAY) # white on gray for buttons 275 | CPS[BUTTON] = curses.color_pair(BUTTON) 276 | curses.init_pair(HLITE,BLACK,GREEN) # black on Green for highlight aps,stas 277 | CPS[HLITE] = curses.color_pair(HLITE) 278 | curses.init_pair(ERR,WHITE,RED) # white on red 279 | CPS[ERR] = curses.color_pair(ERR) 280 | curses.init_pair(WARN,BLACK,YELLOW) # white on yellow 281 | CPS[WARN] = curses.color_pair(WARN) 282 | curses.init_pair(NOTE,WHITE,GREEN) # white on red 283 | CPS[NOTE] = curses.color_pair(NOTE) 284 | 285 | def banner(win): 286 | """ 287 | writes the banner (caller will need to refresh) 288 | :param win: main window 289 | """ 290 | # get the num columns and the longest banner length to find the start column 291 | _,nc = win.getmaxyx() 292 | c = 0 293 | for line in _BANNER_: 294 | if len(line) > c: c = len(line) 295 | c = (nc-c)/2 296 | 297 | # add each line in the banner 298 | for i,line in enumerate(_BANNER_): win.addstr(i+1,c,line,CPS[WHITE]) 299 | 300 | # put the copyright in the middle 301 | copy = "captiv8 v{0} Copyright {1}".format(captiv8.version,captiv8.__date__) 302 | win.addstr(len(_BANNER_)+1,(nc-len(copy))/2,copy,CPS[BLUE]) 303 | 304 | #### WIDGETS SETUP 305 | 306 | def mainmenu(win,s=None): 307 | """ 308 | writes the main menu (caller will need to refresh) 309 | :param win: the main window 310 | :param s: current state, None = invalid 311 | """ 312 | start = len(_BANNER_)+2 313 | win.addstr(start,3, "MENU: choose one",CPS[BLUE]) 314 | 315 | # for each option, set color based on state 316 | # configure 317 | color = CPS[WHITE] 318 | if s == _SCANNING_ or _CONNECTING_ <= s < _OPERATIONAL_: 319 | color = CPS[GRAY] 320 | win.addstr(start+1,5,"[C|c]onfigure",color) 321 | 322 | # run 323 | if s == _SCANNING_: 324 | text = "[S|s]top" 325 | color = CPS[WHITE] 326 | else: 327 | text = "[R|r]un " # add a space to cover Stop 328 | if s == _CONFIGURED_ or s == _STOPPED_: 329 | color = CPS[WHITE] 330 | else: 331 | color = CPS[GRAY] 332 | win.addstr(start+2,5,text,color) 333 | 334 | # view 335 | color = CPS[WHITE] 336 | if s < _SCANNING_: color = CPS[GRAY] 337 | win.addstr(start+3,5,"[V|v]iew",color) 338 | 339 | # quit is always allowed 340 | win.addstr(start+4,5,"[Q|q]uit",CPS[WHITE]) 341 | 342 | def infowindow(win): 343 | """ 344 | create an info window as derived window of main window win 345 | :param win: main window 346 | :returns: tuple t = (info window, info window outputs) 347 | """ 348 | # add a derived window center bottom with 9 rows, & a blue border 349 | nr,nc = win.getmaxyx() 350 | info = win.derwin(9,nc-4,nr-10,2) 351 | y,x = info.getmaxyx() 352 | info.attron(CPS[BLUE]) 353 | info.border(0) 354 | info.attron(CPS[BLUE]) 355 | 356 | # init to None the info window outputs dict 357 | # iws = field -> (start_y, start_x,length) 358 | iws = {'dev':None,'Driver':None,'Mode':None, # source fields 359 | 'MAC':None,'Manuf':None,'Connected':None, 360 | 'SSID':None,'BSSID':None, # target fields 361 | 'STA:':None,'IP':None} 362 | 363 | # add source info labels 364 | info.addstr(0,x-len('SOURCE')-1,'SOURCE',CPS[WHITE]) 365 | row1 = "dev: {0} Driver: {1} Mode: {1}".format('-'*_DEVLEN_,'-'*_FIXLEN_) 366 | row2 = "MAC: {0} Manuf: {1} Connected: -".format('-'*_MACLEN_,'-'*_FIXLEN_) 367 | info.addstr(1,1,row1,CPS[WHITE]) 368 | info.addstr(2,1,row2,CPS[WHITE]) 369 | 370 | # add window output fields 371 | # row 1 372 | r = len('dev: ')+1 373 | iws['dev'] = (1,r,_DEVLEN_) 374 | r += _DEVLEN_ + len(" Driver: ") 375 | iws['Driver'] = (1,r,_FIXLEN_) 376 | r += _FIXLEN_ + len(" Mode: ") 377 | iws['Mode'] = (1,r,_FIXLEN_) 378 | # row 2 379 | r = len("MAC: ")+1 380 | iws['MAC'] = (2,r,_MACLEN_) 381 | r += _MACLEN_ + len(" Manuf: ") 382 | iws['Manuf'] = (2,r,_FIXLEN_) 383 | r += _FIXLEN_ + len(" Connected: ") 384 | iws['Connected'] = (2,r,1) 385 | 386 | # add a horz. line seperating the source and target info panels 387 | info.hline(3,1,curses.ACS_HLINE,x-2) 388 | info.addstr(3,x-len('TARGET')-1,'TARGET',CPS[WHITE]) 389 | 390 | # add our target info labels 391 | info.addstr(4,1,"SSID:",CPS[WHITE]) 392 | info.addstr(4,7,'-'*_SSIDLEN_,CPS[WHITE]) 393 | iws['SSID'] = (4,7,_SSIDLEN_) 394 | info.addstr(4,x-(len("BSSID: ")+_MACLEN_+1),"BSSID:",CPS[WHITE]) 395 | info.addstr(4,x-(_MACLEN_+1),'-'*_MACLEN_,CPS[WHITE]) 396 | iws['BSSID'] = (4,x-(_MACLEN_+1),_MACLEN_) 397 | info.addstr(5,1,"STA:",CPS[WHITE]) 398 | info.addstr(5,7,'-'*_MACLEN_,CPS[WHITE]) 399 | iws['STA'] = (5,7,_MACLEN_) 400 | info.addstr(5,x-(len("IP: ")+_MACLEN_+1),"IP:",CPS[WHITE]) # right align w/ BSSID 401 | info.addstr(5,x-(_IPLEN_+1),'-'*_IPLEN_,CPS[WHITE]) 402 | iws['IP'] = (5,x-(_IPLEN_+1),_IPLEN_) 403 | 404 | # add the current status at bottom left 405 | info.addstr(6,1,"[ ] {0}".format(_STATE_FLAG_NAMES_[_INVALID_]),CPS[WHITE]) 406 | info.addch(6,2,ord('?'),CPS[RED]) 407 | iws['current'] = (6,2,1) 408 | iws['current-msg'] = (6,len("[ ] ")+1,x-len("[ ] ")-2) 409 | iws['data-msg'] = (7,1,x-2) 410 | info.refresh() 411 | return info, iws 412 | 413 | def updatesourceinfo(win,iws,c): 414 | """ 415 | writes current state to info window 416 | :param win: the info window 417 | :param iws: the info window output dict should at minimum the keys 418 | 'dev','Driver','Mode','MAC','Manuf','Connected', 419 | :param c: the current config should be in the form 420 | config = {'SSID':None, 'dev':None, 'connect':None} 421 | """ 422 | # set defaults then check the conf dict for a device 423 | dev = c['dev'] if c['dev'] else '-'*_DEVLEN_ 424 | driver = mode = manuf = '-'*_FIXLEN_ 425 | hwaddr = '-'*_MACLEN_ 426 | conn = '-' 427 | color = CPS[WHITE] 428 | if c['dev']: 429 | try: 430 | card = pyw.getcard(dev) 431 | ifinfo = pyw.ifinfo(card) 432 | driver = ifinfo['driver'][:_FIXLEN_] # trim excess 433 | hwaddr = ifinfo['hwaddr'].upper() 434 | manuf = ifinfo['manufacturer'][:_FIXLEN_] # trim excess 435 | mode = pyw.modeget(card) 436 | conn = 'Y' if pyw.isconnected(card) else 'N' 437 | color = CPS[GREEN] 438 | except pyric.error as _e: 439 | raise error("ERRNO {0}. {1}".format(_e.errno,_e.strerror)) 440 | win.addstr(iws['dev'][0],iws['dev'][1],dev,color) 441 | win.addstr(iws['Driver'][0],iws['Driver'][1],driver,color) 442 | win.addstr(iws['Mode'][0],iws['Mode'][1],mode,color) 443 | win.addstr(iws['MAC'][0],iws['MAC'][1],hwaddr,color) 444 | win.addstr(iws['Manuf'][0],iws['Manuf'][1],manuf,color) 445 | win.addstr(iws['Connected'][0],iws['Connected'][1],conn,color) 446 | 447 | def updatetargetinfo(win,iws,c): 448 | """ 449 | writes current state to info window 450 | :param win: the info window 451 | :param iws: the info window output dict should have at minimum the keys 452 | 'SSID','BSSID','STA','IP' 453 | :param c: the current config should be in the form 454 | config = {'SSID':None, 'dev':None, 'connect':None} 455 | """ 456 | # TODO: have to also pass data concerning any BSSID/STA/IP data once 457 | # connected 458 | # overwrite old ssid with blanks before writing new 459 | win.addstr(iws['SSID'][0],iws['SSID'][1],'-'*_SSIDLEN_,CPS[WHITE]) 460 | if c['SSID']: win.addstr(iws['SSID'][0],iws['SSID'][1],c['SSID'],CPS[GREEN]) 461 | 462 | # noinspection PyUnresolvedReferences 463 | def updatestateinfo(win,iws,s): 464 | """ 465 | writes current state to info window 466 | :param win: the info window 467 | :param iws: the info window output dict should have at minimun the keys 468 | 'current','current-msg' 469 | :param s: current state 470 | """ 471 | color = symbol = None # appease pycharm 472 | if s == _INVALID_ or s == _CONFIGURED_: 473 | color = CPS[RED] 474 | symbol = '?' if s == _INVALID_ else '-' 475 | elif s == _OPERATIONAL_: 476 | color = CPS[GREEN] 477 | symbol = '+' 478 | elif s == _QUITTING_: 479 | color = CPS[GREEN] 480 | symbol = '#' 481 | elif s > _CONFIGURED_: 482 | color = CPS[YELLOW] 483 | symbol = '/' 484 | win.addch(iws['current'][0],iws["current"][1],symbol,color) 485 | win.addstr(iws['current-msg'][0],iws['current-msg'][1],_STATE_FLAG_NAMES_[s],CPS[WHITE]) 486 | win.refresh() 487 | 488 | # noinspection PyUnresolvedReferences 489 | def msgwindow(win,mtype,msg): 490 | """ 491 | shows an error/warning/note msg until user clicks OK 492 | :param win: the main window 493 | :param mtype: message type one of {'err','warn','note'} 494 | :param msg: the message to display 495 | """ 496 | # set max width & line width 497 | nx = 30 498 | llen = nx -2 499 | 500 | # break the message up into lines 501 | lines = [] 502 | line = '' 503 | for word in msg.split(' '): 504 | if len(word) + 1 + len(line) > llen: 505 | lines.append(line.strip()) 506 | line = word 507 | else: 508 | line += ' ' + word 509 | if line: lines.append(line) 510 | 511 | # now calcuate # of rows needed 512 | ny = 4 + len(lines) # 2 for border, 2 for title/btn) 513 | 514 | # determine color scheme and title (set default as error 515 | title = "ERROR" 516 | color = ERR 517 | if mtype == 'warn': 518 | title = "WARNING" 519 | color = WARN 520 | elif mtype == 'note': 521 | title = "NOTE" 522 | color = NOTE 523 | 524 | # create the msg window 525 | nr,nc = win.getmaxyx() 526 | zy = (nr-ny)/2 527 | zx = (nc-nx)/2 528 | msgwin = curses.newwin(ny,nx,zy,zx) 529 | msgwin.bkgd(' ',CPS[color]) 530 | msgwin.attron(color) 531 | msgwin.border(0) 532 | 533 | # display title, message and OK btn 534 | msgwin.addstr(1,(llen-len(title))/2,title) 535 | for i,line in enumerate(lines): msgwin.addstr(i+2,1,line) 536 | btn = "Ok" 537 | btncen = (nx-len(btn))/2 538 | by,bx = ny-2,btncen-(len(btn)-1) 539 | msgwin.addstr(by,bx,btn[0],CPS[BUTTON]|curses.A_UNDERLINE) 540 | msgwin.addstr(by,bx+1,btn[1:],CPS[BUTTON]) 541 | bs = (by+zy,bx+zx,2) 542 | 543 | # show the win, and take keypad & loop until OK'd 544 | msgwin.refresh() 545 | msgwin.keypad(1) 546 | while True: 547 | _ev = msgwin.getch() 548 | if _ev == curses.KEY_MOUSE: 549 | try: 550 | _,mx,my,_,b = curses.getmouse() 551 | if b == curses.BUTTON1_CLICKED: 552 | if my == bs[0] and (bs[1] <= mx <= bs[1] + bs[2]): break 553 | except curses.error: 554 | continue 555 | else: 556 | try: 557 | _ch = chr(_ev).upper() 558 | except ValueError: 559 | continue 560 | if _ch == 'O': break 561 | del msgwin 562 | win.touchwin() 563 | win.refresh() 564 | 565 | def waitwindow(win,ttl,msg): 566 | """ 567 | displays a blocking window w/ message 568 | :param win: the main window 569 | :param ttl: the title (must be less than nx) 570 | :param msg: the message 571 | :returns: the wait window 572 | """ 573 | # set max width & line width 574 | nx = 30 575 | llen = nx -2 576 | 577 | # break the message up into lines 578 | lines = [] 579 | line = '' 580 | for word in msg.split(' '): 581 | if len(word) + 1 + len(line) > llen: 582 | lines.append(line.strip()) 583 | line = word 584 | else: 585 | line += ' ' + word 586 | if line: lines.append(line) 587 | 588 | # now calcuate # of rows needed 589 | ny = 3 + len(lines) # 2 for border, 1 for title) 590 | 591 | # create the wait window with a red border 592 | nr,nc = win.getmaxyx() 593 | zy = (nr-ny)/2 594 | zx = (nc-nx)/2 595 | waitwin = curses.newwin(ny,nx,zy,zx) 596 | waitwin.bkgd(' ',CPS[NOTE]) 597 | waitwin.attron(NOTE) 598 | waitwin.border(0) 599 | 600 | # display title, message and OK btn 601 | ttl = ttl[:llen] 602 | waitwin.addstr(1,(llen-len(ttl))/2,ttl) 603 | for i,line in enumerate(lines): waitwin.addstr(i+2,1,line) 604 | 605 | # show the win, and take the keypad 606 | waitwin.refresh() 607 | waitwin.keypad(1) 608 | return waitwin 609 | 610 | #### MENU OPTION CALLBACKS 611 | 612 | # noinspection PyUnresolvedReferences 613 | def configure(win,conf): 614 | """ 615 | shows options to configure captiv8 for running 616 | :param win: the main window 617 | :param conf: current state of configuration dict 618 | """ 619 | # create our on/off for radio buttons 620 | BON = curses.ACS_DIAMOND 621 | BOFF = '_' 622 | 623 | # create an inputs dict to hold the begin locations of inputs 624 | ins = {} # input -> (start_y,start_x,endx) 625 | 626 | # create a copy of conf to manipulate 627 | newconf = {} 628 | for c in conf: newconf[c] = conf[c] 629 | 630 | # create new window (new window will cover the main window) 631 | # get sizes for coord translation 632 | nr,nc = win.getmaxyx() # size of the main window 633 | ny,nx = 15,50 # size of new window 634 | zy,zx = (nr-ny)/2,(nc-nx)/2 # 0,0 (top left corner) of new window 635 | confwin = curses.newwin(ny,nx,zy,zx) 636 | 637 | # draw a blue border and write title 638 | confwin.attron(CPS[BLUE]) 639 | confwin.border(0) 640 | confwin.attron(CPS[BLUE]) 641 | confwin.addstr(1,1,"Configure Options",CPS[BLUE]) 642 | 643 | # ssid option, add if present add a clear button to the right 644 | confwin.addstr(2,1,"SSID: " + '_'*_SSIDLEN_,CPS[WHITE]) 645 | ins['SSID'] = (2+zy,len("SSID: ")+zx+1,len("SSID: ")+zx+_SSIDLEN_) 646 | if newconf['SSID']: 647 | for i,s in enumerate(newconf['SSID']): 648 | confwin.addch(ins['SSID'][0]-zy,ins['SSID'][1]-zx+i,s,CPS[GREEN]) 649 | 650 | # allow for up to 6 devices to choose in rows of 2 x 3 651 | confwin.addstr(3,1,"Select dev:",CPS[WHITE]) # the sub title 652 | i = 4 # current row 653 | j = 0 # current dev 654 | devs = pyw.winterfaces()[:8] 655 | if not newconf['dev'] in devs: newconf['dev'] = None 656 | for dev in devs: 657 | stds = "" 658 | monitor = True 659 | nl80211 = True 660 | try: 661 | card = pyw.getcard(dev) 662 | stds = pyw.devstds(card) 663 | monitor = 'monitor' in pyw.devmodes(card) 664 | except pyric.error: 665 | # assume just related to current dev 666 | nl80211 = False 667 | devopt = "{0}. (_) {1}".format(j+1,dev) 668 | if stds: devopt += " IEEE 802.11{0}".format(''.join(stds)) 669 | if monitor and nl80211: 670 | confwin.addstr(i,2,devopt,CPS[WHITE]) 671 | ins[j] = (i+zy,len("n. (")+zx+2,len("n. (")+zx+3) 672 | if newconf['dev'] == dev: 673 | confwin.addch(ins[j][0]-zy,ins[j][1]-zx,BON,CPS[GREEN]) 674 | else: 675 | # make it gray 676 | errmsg = "" 677 | if not monitor: errmsg = "No monitor mode" 678 | elif not nl80211: errmsg = "No nl80211" 679 | confwin.addstr(i,2,devopt,CPS[GRAY]) 680 | confwin.addstr(i,3,'X',CPS[GRAY]) 681 | confwin.addstr(i,len(devopt)+3,errmsg,CPS[GRAY]) 682 | i += 1 683 | j += 1 684 | 685 | # connect option, select current if present 686 | confwin.addstr(i,1,"Connect: (_) auto (_) manual",CPS[WHITE]) 687 | ins['auto'] = (i+zy,len("Connect: (")+zx+1,len("Connect: (")+zx+2) 688 | ins['manual'] = (i+zy, 689 | len("Connect: (_) auto (")+zx+1, 690 | len("Connect: (_) auto (")+zx+2) 691 | if newconf['connect']: 692 | confwin.addch(ins[newconf['connect']][0]-zy, 693 | ins[newconf['connect']][1]-zx, 694 | BON,CPS[GREEN]) 695 | 696 | # we want two buttons Set and Cancel. Make these buttons centered. Underline 697 | # the first character 698 | btn1 = "Set" 699 | btn2 = "Cancel" 700 | btnlen = len(btn1) + len(btn2) + 1 # add a space 701 | btncen = (nx-btnlen) / 2 # center point for both 702 | # btn 1 -> underline first character 703 | y,x = ny-2,btncen-(len(btn1)-1) 704 | confwin.addstr(y,x,btn1[0],CPS[BUTTON]|curses.A_UNDERLINE) 705 | confwin.addstr(y,x+1,btn1[1:],CPS[BUTTON]) 706 | ins['set'] = (y+zy,x+zx,x+zx+len(btn1)-1) 707 | # btn 2 -> underline first character 708 | y,x = ny-2,btncen+2 709 | confwin.addstr(y,x,btn2[0],CPS[BUTTON]|curses.A_UNDERLINE) 710 | confwin.addstr(y,x+1,btn2[1:],CPS[BUTTON]) 711 | ins['cancel'] = (y+zy,x+zx,x+zx+len(btn2)-1) 712 | confwin.refresh() 713 | 714 | # capture the focus and run our execution loop 715 | confwin.keypad(1) # enable IOT read mouse events 716 | store = False 717 | while True: 718 | _ev = confwin.getch() 719 | if _ev == curses.KEY_MOUSE: 720 | # handle mouse, determine if we should check/uncheck etc 721 | try: 722 | _,mx,my,_,b = curses.getmouse() 723 | except curses.error: 724 | continue 725 | 726 | if b == curses.BUTTON1_CLICKED: 727 | # determine if we're inside a option area 728 | if my == ins['set'][0]: 729 | if ins['set'][1] <= mx <= ins['set'][2]: 730 | store = True 731 | break 732 | elif ins['cancel'][1] <= mx <= ins['cancel'][2]: 733 | break 734 | elif my == ins['SSID'][0]: 735 | if ins['SSID'][1] <= mx <= ins['SSID'][2]: 736 | # move the cursor to the first entry char & turn on 737 | curs = ins['SSID'][0],ins['SSID'][1] 738 | confwin.move(curs[0]-zy,curs[1]-zx) 739 | curses.curs_set(1) 740 | 741 | # loop until we get 742 | while True: 743 | # get the next char 744 | _ev = confwin.getch() 745 | if _ev == ascii.NL or _ev == curses.KEY_ENTER: break 746 | elif _ev == ascii.BS or _ev == curses.KEY_BACKSPACE: 747 | if curs[1] == ins['SSID'][1]: continue 748 | # delete (write over with '-') prev char, then move back 749 | curs = curs[0],curs[1]-1 750 | confwin.addch(curs[0]-zy, 751 | curs[1]-zx, 752 | BOFF, 753 | CPS[WHITE]) 754 | confwin.move(curs[0]-zy,curs[1]-zx) 755 | else: 756 | if curs[1] > ins['SSID'][2]: 757 | curses.flash() 758 | continue 759 | 760 | # add the character, (cursor moves on its own) 761 | # update our pointer for the next entry 762 | try: 763 | confwin.addstr(curs[0]-zy, 764 | curs[1]-zx, 765 | chr(_ev), 766 | CPS[GREEN]) 767 | curs = curs[0],curs[1]+1 768 | except ValueError: 769 | # put this back on and see if the outer 770 | # loop can do something with it 771 | curses.ungetch(_ev) 772 | break 773 | curses.curs_set(0) # turn off the cursor 774 | elif my == ins['auto'][0]: 775 | if ins['auto'][1] <= mx <= ins['auto'][2]: 776 | if newconf['connect'] == 'manual': 777 | # turn off manual 778 | confwin.addch(ins['manual'][0]-zy, 779 | ins['manual'][1]-zx, 780 | BOFF,CPS[WHITE]) 781 | newconf['connect'] = 'auto' 782 | confwin.addch(my-zy,mx-zx,BON,CPS[GREEN]) 783 | confwin.refresh() 784 | elif ins['manual'][1] <= mx <= ins['manual'][2]: 785 | if newconf['connect'] == 'auto': 786 | # turn off auto 787 | confwin.addch(ins['auto'][0]-zy, 788 | ins['auto'][1]-zx, 789 | BOFF,CPS[WHITE]) 790 | newconf['connect'] = 'manual' 791 | confwin.addch(my-zy,mx-zx,BON,CPS[GREEN]) 792 | confwin.refresh() 793 | else: 794 | # check for each listed device 795 | for d in range(j): 796 | if my == ins[d][0] and ins[d][1] <= mx <= ins[d][2]: 797 | # check the selected dev 798 | confwin.addch(my-zy,mx-zx,BON,CPS[GREEN]) 799 | 800 | # determine if a previously selected needs to be unchecked 801 | if newconf['dev'] is None: pass 802 | elif newconf['dev'] != devs[d]: 803 | i = devs.index(newconf['dev']) 804 | confwin.addch(ins[i][0]-zy, 805 | ins[i][1]-zx, 806 | BOFF, 807 | CPS[WHITE]) 808 | newconf['dev'] = devs[d] 809 | confwin.refresh() 810 | break # exit the for loop 811 | else: 812 | try: 813 | _ch = chr(_ev).upper() 814 | except ValueError: 815 | continue 816 | if _ch == 'S': 817 | store = True 818 | break 819 | elif _ch == 'C': break 820 | elif _ch == 'L': 821 | pass 822 | 823 | # only 'radio buttons' are kept, check if a SSID was entered and add if so 824 | if store: 825 | ssid = confwin.instr(ins['SSID'][0]-zy,ins['SSID'][1]-zx,_SSIDLEN_) 826 | ssid = ssid.strip('_').strip() # remove training lines, & spaces 827 | if ssid: newconf['SSID'] = ssid 828 | 829 | # delete this window and return 830 | del confwin # remove the window 831 | return newconf if store else None 832 | 833 | # noinspection PyUnresolvedReferences 834 | def view(win,nets): 835 | """ 836 | displays stats on collected entities 837 | :param win: the main window 838 | :param nets: the network dict 839 | """ 840 | # create new window (new window will cover the main window) 841 | nr,nc = win.getmaxyx() # size of the main window 842 | ny,nx = 19,60 # size of new window 843 | zy,zx = 1,(nc-nx)/2 # 0,0 (top left corner) of new window 844 | viewwin = curses.newwin(ny,nx,zy,zx) # draw it 845 | viewwin.attron(CPS[GREEN]) # and add a green border 846 | viewwin.border(0) 847 | viewwin.attroff(CPS[GREEN]) # this doesn't seem to have an effect 848 | 849 | # inputs dict to hold the begin locations of inputs 850 | ins = {} # input -> (start_y,start_x,end_x) 851 | 852 | # size/location variables 853 | ystart = 5 854 | apRows = 5 855 | staRows = 10 856 | 857 | # add subtitle and data title lines 858 | # left side (APs) 859 | lsub = "APs" 860 | lttl = "BSSID RSS CH #" 861 | lL = len(lttl) # length of left title 862 | viewwin.addstr(2,(lL-len(lsub))/2+1,lsub) 863 | viewwin.addstr(3,1,lttl) 864 | viewwin.hline(4,1,curses.ACS_HLINE,lL,CPS[GREEN]) 865 | viewwin.addch(4,lL+1,curses.ACS_UARROW,CPS[BUTTON]) 866 | ins['aup'] = (4+zy,lL+1+zx,lL+1+zx) 867 | 868 | # right side (Clients) 869 | rx = lL+3 # length of leftsize w/ border and center elements 870 | rsub = "Clients" 871 | rttl = "STA (MAC) RSS S/T" 872 | lR = len(rttl) 873 | viewwin.addstr(2,(lR-len(rsub))/2+rx,rsub) 874 | viewwin.addstr(3,rx,rttl) 875 | viewwin.hline(4,rx,curses.ACS_HLINE,lR,CPS[GREEN]) 876 | viewwin.addch(4,lR+rx,curses.ACS_UARROW,CPS[BUTTON]) 877 | ins['sup'] = (4+zy,lR+rx+zx,lR+rx+zx) 878 | 879 | # add footers w/ scroll down buttons 880 | viewwin.hline(ystart+apRows,1,curses.ACS_HLINE,lL,CPS[GREEN]) 881 | viewwin.addstr(ystart+apRows+1,1,"APs:") 882 | ins['numAPs'] = (ystart+apRows+1,len("APs:")+1,lL) 883 | viewwin.addstr(ystart+apRows+2,1,"Clients:") 884 | ins['numClts'] = (ystart+apRows+2,len("Clients:")+1,lL) 885 | viewwin.hline(ystart+staRows,1,curses.ACS_HLINE,lL+1,CPS[GREEN]) 886 | viewwin.addch(ystart+apRows,lL+1,'v',CPS[BUTTON]) 887 | ins['adown'] = (ystart+apRows+zy,lL+1+zx,lL+1+zx) 888 | viewwin.hline(ystart+staRows,rx,curses.ACS_HLINE,lR,CPS[GREEN]) 889 | viewwin.addch(ystart+staRows,lR+rx,'v',CPS[BUTTON]) 890 | ins['sdown'] = (ystart+staRows+zy,lR+rx+zx,lR+rx+zx) 891 | 892 | # along vertical path of scroll areas, draw a gray checkerboard 893 | for y in range(ystart,ystart+apRows): 894 | viewwin.addch(y, lL + 1, curses.ACS_CKBOARD, CPS[GRAY]) 895 | for y in range(ystart,ystart+staRows): 896 | viewwin.addch(y,lR+rx,curses.ACS_CKBOARD,CPS[GRAY]) 897 | 898 | # draw a vertical line down the center from data title to data footer 899 | y = None # appease pycharm 900 | for y in range(3,ystart+staRows): 901 | viewwin.addch(y,lL+2,curses.ACS_VLINE,CPS[GREEN]) 902 | viewwin.addch(y+1,lL+2,'#',CPS[GREEN]) 903 | 904 | # add the title and OK button. We want to center them on the subdivde where 905 | # APs & clients. They won't be centered then but will appear so 906 | title = "View" 907 | viewwin.addstr(1,lL,title) 908 | btn = "Ok" 909 | viewwin.addstr(ny-2,lL+1,btn[0],CPS[BUTTON]|curses.A_UNDERLINE) 910 | viewwin.addstr(ny-2,lL+2,btn[1:],CPS[BUTTON]) 911 | ins['ok'] = (ny-2+zy,lL+1+zx,lL+1+zx+2) 912 | 913 | # create the ap pad (rows x width of lttl) 914 | bssids = nets.keys() # list of initial bssid keys append as new ones come in 915 | lsA = [] # list of initial ap lines to write 916 | maxA = 30 # maximum 30 bssids (should never reach) 917 | curA = 0 # cur index into ap list 918 | selA = None # selected index into ap list 919 | aly,alx,ary,arx = zy+ystart,zx+1,zy+ystart+apRows-1,zx+lL 920 | apad = curses.newpad(maxA,lL) 921 | 922 | # create the clients pad (rows x width of rttl) 923 | lsS = [] # list of sta lines to write 924 | maxS = 99 # maximum 99 stas 925 | curS = 0 # current index into sta list 926 | selS = None # selected index into sta list 927 | sly,slx,sry,srx = zy+ystart,rx+zx,zy+ystart+staRows-1,zx+rx+lR 928 | spad = curses.newpad(maxS,lR) 929 | 930 | # fill the ap pad with any initial data (& count ttl number of clients) 931 | clnts = 0 932 | for i,bssid in enumerate(bssids): 933 | if i > maxA: break 934 | rss = nets[bssid]['rss'] 935 | if rss is None: rss = '---' 936 | elif rss < -99: rss = -99 937 | ch = nets[bssid]['ch'] 938 | if not ch: ch = '---' 939 | nC = len(nets[bssid]['stas']) 940 | if nc > maxS: nc = maxS 941 | lsA.append("{} {:>3} {:>3} {:>2}".format(bssid,rss,ch,nC)) 942 | apad.addstr(i,0,lsA[i]) 943 | 944 | # update the count of APs and clients 945 | viewwin.addstr(ins['numAPs'][0],ins['numAPs'][1]," {0}".format(len(nets))) 946 | viewwin.addstr(ins['numClts'][0],ins['numClts'][1]," {0}".format(clnts)) 947 | 948 | # take the keyboard, and show this windown prior to refreshing the pads 949 | # Move both pads to translated coordinates IOT put them "inside" 950 | # the window [y,x upperleft of pad, 951 | # y1,x1 upperleft of win, 952 | # y2,x2 lowerright of win] 953 | viewwin.keypad(1) 954 | viewwin.refresh() 955 | apad.refresh(curA,0,aly,alx,ary,arx) 956 | spad.refresh(curS,0,sly,slx,sry,srx) 957 | 958 | # show and loop until ok'd 959 | while True: 960 | # check for user input 961 | _ev = viewwin.getch() 962 | if _ev == curses.KEY_MOUSE: 963 | try: 964 | _,mx,my,_,b = curses.getmouse() 965 | if b == curses.BUTTON1_CLICKED: 966 | if aly <= my <= ary and alx <= mx <= arx: # w/in AP range 967 | i = (my-aly)+curA # index into lsA of selection 968 | try: 969 | # unselect any previously selected bssid & remove 970 | # any clients currently shown 971 | if selA is not None: 972 | apad.addstr(selA,0,lsA[selA],CPS[WHITE]) 973 | for j,_ in enumerate(lsS): 974 | spad.addstr(j,0,' '*lR) 975 | lsS = [] 976 | 977 | # select new (or set to none if deselecting) 978 | if selA == i: selA = None 979 | else: 980 | selA = i 981 | apad.addstr(selA,0,lsA[selA],CPS[HLITE]) 982 | 983 | # show this bssids clients 984 | bssid = bssids[selA] 985 | for j,sta in enumerate(nets[bssid]['stas']): 986 | rss = nets[bssid]['stas'][sta]['rss'] 987 | if not rss: rss = '---' 988 | t = nets[bssid]['stas'][sta]['spoofed'] 989 | s = nets[bssid]['stas'][sta]['success'] 990 | spt = "{}/{}".format(s,t) 991 | lsS.append("{} {:>3} {:>5}".format(sta,rss,spt)) 992 | spad.addstr(j,0,lsS[j]) 993 | 994 | # refresh the bssid & client pads 995 | apad.refresh(curA,0,aly,alx,ary,arx) 996 | spad.refresh(curS,0,sly,slx,sry,srx) 997 | except (IndexError,KeyError): 998 | continue 999 | elif my == ins['ok'][0]: 1000 | if ins['ok'][1] <= mx <= ins['ok'][2]: break 1001 | elif (my,mx) == (ins['aup'][0],ins['aup'][1]): 1002 | if curA == 0: continue 1003 | curA -= 1 1004 | apad.refresh(curA,0,aly,alx,ary,arx) 1005 | elif (my,mx) == (ins['adown'][0],ins['adown'][1]): 1006 | if curA >= len(bssids)-apRows: continue 1007 | curA += 1 1008 | apad.refresh(curA,0,aly,alx,ary,arx) 1009 | elif (my,mx) == (ins['aup'][0],ins['sup'][1]): break 1010 | elif (my,mx) == (ins['sdown'][0],ins['sdown'][1]): break 1011 | except curses.error: 1012 | continue 1013 | else: 1014 | try: 1015 | _ch = chr(_ev).upper() 1016 | except ValueError: 1017 | continue 1018 | if _ch == 'O': break 1019 | del viewwin 1020 | win.touchwin() 1021 | win.refresh() 1022 | 1023 | if __name__ == '__main__': 1024 | if os.geteuid() != 0: sys.exit("Oops. captiv8 must be run as root") 1025 | # ui variables 1026 | # we make state, aps and stas dicts so the update threads can see them 1027 | err = None 1028 | mainwin = infowin = None 1029 | dS = {'state':_INVALID_} 1030 | config = {'SSID':None,'dev':None,'connect': None} 1031 | #nets = {} 1032 | 1033 | nets = {'d8:c7:c8:f3:fb:60': {'ch': None, 'stas': {}, 'rss': -75}, 1034 | 'd8:c7:c8:f4:00:10': {'ch': None, 'stas': {}, 'rss': -90}, 1035 | 'd8:c7:c8:f3:fa:10': {'ch': None, 'stas': {}, 'rss': -91}, 1036 | 'd8:c7:c8:f3:ff:60': {'ch': None, 'stas': {}, 'rss': -86}, 1037 | 'd8:c7:c8:f4:00:60': {'ch': 11, 'stas': { 1038 | 'f4:09:d8:88:ed:63': {'spoofed': 0, 'ts': 1472964477.522748, 1039 | 'success': 0, 'rss': -79}}, 'rss': -87}, 1040 | 'd8:c7:c8:f3:ff:80': {'ch': 11, 'stas': { 1041 | 'c0:cc:f8:19:2f:17': {'spoofed': 0, 'ts': 1472964500.180947, 1042 | 'success': 0, 'rss': -21}, 1043 | '40:f0:2f:cb:ca:f3': {'spoofed': 0, 'ts': 1472964499.963706, 1044 | 'success': 0, 'rss': -68}, 1045 | 'ac:b5:7d:14:54:76': {'spoofed': 0, 'ts': 1472964499.998591, 1046 | 'success': 0, 'rss': -74}, 1047 | 'c8:ff:28:31:6a:4b': {'spoofed': 0, 'ts': 1472964455.113911, 1048 | 'success': 0, 'rss': -63}, 1049 | 'e8:61:7e:6c:a4:e7': {'spoofed': 0, 'ts': 1472964477.567793, 1050 | 'success': 0, 'rss': None}}, 'rss': -37}, 1051 | 'd8:c7:c8:f3:ff:b0': {'ch': 1, 'stas': { 1052 | 'c4:8e:8f:a6:79:51': {'spoofed': 0, 'ts': 1472964426.521016, 1053 | 'success': 0, 'rss': -83}, 1054 | 'f8:cf:c5:84:71:43': {'spoofed': 0, 'ts': 1472964448.963282, 1055 | 'success': 0, 'rss': -91}}, 'rss': -87}, 1056 | 'd8:c7:c8:f3:fc:f0': {'ch': None, 'stas': {}, 'rss': -89}, 1057 | 'd8:c7:c8:f3:ff:88': {'ch': 153, 'stas': { 1058 | '5c:c5:d4:27:42:e6': {'spoofed': 0, 'ts': 1472964379.929878, 1059 | 'success': 0, 'rss': -74}, 1060 | '54:4e:90:10:92:8e': {'spoofed': 0, 'ts': 1472964491.085011, 1061 | 'success': 0, 'rss': -78}, 1062 | '94:65:9c:73:06:ea': {'spoofed': 0, 'ts': 1472964423.827202, 1063 | 'success': 0, 'rss': -59}, 1064 | 'd8:fc:93:8b:13:ac': {'spoofed': 0, 'ts': 1472964490.59065, 1065 | 'success': 0, 'rss': -77}, 1066 | 'a0:cb:fd:7b:c9:4c': {'spoofed': 0, 'ts': 1472964468.864295, 1067 | 'success': 0, 'rss': -59}}, 'rss': -47}} 1068 | 1069 | # helpers 1070 | c1 = c2 = None # pipe ends for collector comms 1071 | dq = mp.Queue() # data queue for collect, data updater 1072 | ublock = threading.Event() # event to block updating threads 1073 | infoupdater = None # the info message updater 1074 | dataupdate = None # the data dict updater 1075 | collector = None # the collector 1076 | 1077 | # catch curses, runtime and ctrl-c 1078 | try: 1079 | # get the windows up 1080 | mainwin = setup() 1081 | infowin,iwfs = infowindow(mainwin) 1082 | mainwin.refresh() 1083 | 1084 | # create the info updater thread then the data updater thread 1085 | infoupdater = InfoUpdateThread(infowin,ublock,dS,iwfs) 1086 | infoupdater.start() 1087 | dataupdater = DataUpdateThread(infowin,ublock,dq,nets,dS,iwfs) 1088 | dataupdater.start() 1089 | 1090 | # execution loop 1091 | while True: 1092 | try: 1093 | ev = mainwin.getch() 1094 | if ev == curses.KEY_MOUSE: pass 1095 | else: 1096 | ch = chr(ev).upper() 1097 | if ch == 'C': 1098 | if dS['state'] == _SCANNING_ or _CONNECTING_ <= dS['state'] < _OPERATIONAL_: 1099 | msgwindow(mainwin,'warn',"Cannot configure while running") 1100 | continue 1101 | newconfig = configure(mainwin,config) 1102 | if newconfig and cmp(newconfig,config) != 0: 1103 | config = newconfig 1104 | complete = True 1105 | for key in config: 1106 | if config[key] is None: 1107 | complete = False 1108 | break 1109 | if complete: 1110 | dS['state'] = _CONFIGURED_ 1111 | updatestateinfo(infowin,iwfs,dS['state']) 1112 | mainmenu(mainwin,dS['state']) 1113 | updatesourceinfo(infowin,iwfs,config) 1114 | updatetargetinfo(infowin,iwfs,config) 1115 | mainwin.touchwin() 1116 | mainwin.refresh() 1117 | elif ch == 'R': 1118 | # only allow run if state is configured, or stopped 1119 | wwin = None 1120 | if dS['state'] == _CONFIGURED_ or dS['state'] == _STOPPED_: 1121 | # show a waitwindow while setting up collector 1122 | msg = "Please wait. Preparing {0}".format(config['dev']) 1123 | wwin = waitwindow(mainwin,"Preparing Device",msg) 1124 | mainwin.nodelay(True) # turn off blocking getch 1125 | c1,c2 = mp.Pipe() 1126 | try: 1127 | # break nets into aps and stas dict 1128 | aps = {} 1129 | stas = {} 1130 | for bssid in nets: 1131 | aps[bssid] = nets[bssid]['rss'] 1132 | for sta in nets['stas']: 1133 | stas[sta] = { 1134 | 'ASW':bssid, 1135 | 'ts':nets[bssid]['stas'][sta]['ts'], 1136 | 'rss':nets[bssid]['stas'][sta]['rss'], 1137 | 'rf':ch2rf(nets[bssid]['ch']) 1138 | } 1139 | collector = collect.Collector(c2, 1140 | dq, 1141 | config['SSID'], 1142 | config['dev'], 1143 | aps, 1144 | stas) 1145 | except RuntimeError as e: 1146 | if wwin: 1147 | del wwin 1148 | mainwin.touchwin() 1149 | mainwin.refresh() 1150 | wwin = None 1151 | msgwindow(mainwin,'err',e.message) 1152 | else: 1153 | dS['state'] = _SCANNING_ 1154 | collector.start() 1155 | mainmenu(mainwin,dS['state']) 1156 | del wwin 1157 | mainwin.touchwin() 1158 | mainwin.refresh() 1159 | ublock.set() # unblock the updaters 1160 | else: 1161 | msgwindow(mainwin,'warn',"Cannot run. Not Configured") 1162 | elif ch == 'S': 1163 | if dS['state'] >= _SCANNING_ and dS['state'] != _STOPPED_: 1164 | if c1: c1.send('!QUIT!') 1165 | while mp.active_children(): time.sleep(1) 1166 | ublock.clear() 1167 | c1.close() 1168 | c1 = c2 = collector = None 1169 | mainwin.nodelay(False) 1170 | dS['state'] = _STOPPED_ 1171 | mainmenu(mainwin,dS['state']) 1172 | updatestateinfo(infowin,iwfs,dS['state']) 1173 | elif ch == 'V': 1174 | # only allow view if state is scanning or higher 1175 | #if dS['state'] < _SCANNING_: 1176 | # msgwindow(mainwin,'warn',"Cannot view. Nothing to see") 1177 | # continue 1178 | view(mainwin,nets) 1179 | # once we add this, we'll have to block the infoupdater 1180 | elif ch == 'Q': 1181 | if dS['state'] >= _SCANNING_ and dS['state'] != _STOPPED_: 1182 | if c1: c1.send('!QUIT!') 1183 | while mp.active_children(): time.sleep(1) 1184 | ublock.clear() 1185 | c1 = c2 = collector = None 1186 | mainwin.nodelay(False) 1187 | dS['state'] = _QUITTING_ 1188 | mainmenu(mainwin,dS['state']) 1189 | updatestateinfo(infowin, iwfs, dS['state']) 1190 | ublock.set() # let the updaters catch _QUITTING_ 1191 | break # get out of the loop 1192 | except ValueError: 1193 | # most likely out of range errors from chr 1194 | pass 1195 | except error as e: 1196 | msgwindow(mainwin,'err',e) 1197 | except KeyboardInterrupt: pass 1198 | except RuntimeError as e: err = e 1199 | except curses.error as e: err = e 1200 | finally: 1201 | # set state to quitting and unblock if necessary 1202 | dS['state'] = _QUITTING_ 1203 | if not ublock.is_set(): ublock.set() 1204 | if infoupdater: infoupdater.join(5.0) 1205 | teardown(mainwin) 1206 | if err: sys.exit(err) --------------------------------------------------------------------------------