├── LICENSE ├── README.md ├── conf.json └── rollmac.py /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 violentshell 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Rollmac 2 | Free WiFi networks often impose either a time or data restriction and this can be used quickly. When this happens a user can often change thier MAC address and reconnect to reset the limitation, but this is annoying, and it takes time. In addition, most networks will ask the user to re-accept the terms and conditions of the network in order to continue. 3 | 4 | Rollmac is designed to automate this process by changing the MAC address, then using the WPAD protocol to discover the login page and automatically re-accept the terms and conditions. It also maintains a watch of the network current usage and/or time limit to ensure it is never reached. This means you can run downloads overnight or while you are away from your computer, automatically rolling mac's and reconnecting to the free network. 5 | 6 | The entire recconnect operation usually takes about 10 seconds. 7 | 8 | You may need to configure the script slightly to adjust to individual network specifics, however, Rollmac allows you to download massive amounts of data without user input by setting the conf file and leaving it running overnight. 9 | 10 | 11 | The program is controlled by variables inside the conf.json file. Modify these to meet your network/host machine: 12 | ----- 13 | Set to network ssid (Must have matching profile in "netsh wlan show profiles": 14 | 15 | ssid = 'Free WiFi' 16 | 17 | Set to data limit inMB or 99999999999 for ifinite: 18 | 19 | MB_limit = 250 20 | 21 | Set to time limit in mins or 99999999999 for infinite: 22 | 23 | TIME_limit = 60 24 | 25 | Set to your wireless interface name: 26 | 27 | interface = 'Wireless Local Area Connection' 28 | 29 | Set to the domain of the network you are joining (You can get it from ipconfig /all): 30 | 31 | domain = 'freewifi.com' 32 | 33 | You may want to change this value to 1 to stop ie/browser opening again on each reconnect: 34 | 35 | # 'HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\Internet Settings\WPAD\WpadOverride' 36 | -------------------------------------------------------------------------------- /conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "ssid": "Free WiFi", 3 | 4 | "MB_limit": 250, 5 | 6 | "TIME_limit": 60, 7 | 8 | "interface":"Wireless", 9 | 10 | "domain": "freewifi.com" 11 | } 12 | -------------------------------------------------------------------------------- /rollmac.py: -------------------------------------------------------------------------------- 1 | import bs4 as soup 2 | import datetime 3 | import logging 4 | import psutil 5 | import requests 6 | import spoofmac 7 | import subprocess 8 | import sys 9 | import time 10 | import jconf 11 | 12 | 13 | # noinspection PyAttributeOutsideInit 14 | class RollMac: 15 | def __init__(self): 16 | self.setup_logging() 17 | self.get_config() 18 | self.info = {'domain': self.config.domain} 19 | self.over_limit = True 20 | 21 | def run(self): 22 | # Will hit on first loop, then on over limit 23 | if self.over_limit: 24 | 25 | # Spoof Mac 26 | self.spoof() 27 | 28 | # Let interface come back up 29 | time.sleep(1) 30 | 31 | # Re-connect 32 | self.connect() 33 | 34 | # Get redirect url from wpad 35 | self.get_redirect() 36 | 37 | # login to page with post values 38 | if self.login() == 200: 39 | self.log.info('Reconnected to AP with new mac. Resetting data limit') 40 | 41 | # Reset limits (Data is done automatically by flapping interface) 42 | self.over_limit = False 43 | self.start_time = datetime.datetime.now() 44 | 45 | elif not self.over_limit: 46 | time.sleep(15) 47 | 48 | # Print output and check limits 49 | self.over_limit = self.limit_check() 50 | 51 | def spoof(self): 52 | """ 53 | :return: Nothing. Sets mac on interface 54 | """ 55 | self.mac = spoofmac.random_mac_address() 56 | spoofmac.set_interface_mac(self.config.interface, self.mac) 57 | self.log.debug('Changed mac of {} to: {}'.format(self.config.interface, self.mac)) 58 | 59 | def limit_check(self): 60 | """ 61 | :return: True if within 5% of MB limit; False if outside 5% of MB limit 62 | """ 63 | 64 | # Check time limit and MB limit 65 | if self.time_check() or self.data_check(): 66 | return True 67 | return False 68 | 69 | def time_check(self): 70 | """ 71 | :return: True if within 5 min of limit; False if outside 5 min of limit 72 | """ 73 | 74 | try: 75 | self.start_time 76 | except NameError: 77 | self.start_time = datetime.datetime.now() 78 | self.log.info("Connected at {}".format(datetime.datetime.now().strftime('%H:%M:%S'))) 79 | 80 | # Online time in minutes 81 | online_time = (datetime.datetime.now() - self.start_time).seconds / 60 82 | 83 | # For info 84 | self.log.info('{} minutes remaining'.format(round(self.config.time_limit - online_time - 5))) 85 | 86 | # Return True if over limit threshold 87 | if self.config.time_limit - online_time <= 5: 88 | self.log.warn('Over time limit.') 89 | return True 90 | return False 91 | 92 | def data_check(self): 93 | # Raw interface bytes TODO Linux compatible? 94 | netbytes = psutil.net_io_counters(True)[self.config.interface] 95 | 96 | # Convert total to MB 97 | t_mbytes = (netbytes.bytes_recv + netbytes.bytes_sent) >> 20 98 | 99 | # For info 100 | self.log.debug('{} MB remaining'.format(self.config.limit - t_mbytes)) 101 | 102 | # Return True if over limit threshold 103 | if t_mbytes >= (self.config.limit - (5 / 100 * self.config.limit)): 104 | self.log.warn('Over data limit.') 105 | return True 106 | return False 107 | 108 | def connect(self): 109 | """ 110 | :return: True if connected, False for fail 111 | """ 112 | 113 | netsh = subprocess.check_output('netsh wlan connect name="{}"'.format(self.config.ssid)) 114 | if b'Connection request was completed successfully.' in netsh: 115 | return self.log.debug('Re-assosiated with {}'.format(self.config.ssid)) 116 | 117 | def retry_get(self, url): 118 | """ 119 | :param url: string of a url to fetch 120 | :return: data if it works, raises error if not 121 | """ 122 | for i in range(5): 123 | try: 124 | data = requests.get(url) 125 | return data 126 | except: 127 | self.log.warn('Get failed for {} try number {}'.format(url, i)) 128 | time.sleep(3) 129 | 130 | raise ConnectionError 131 | 132 | def get_redirect(self): 133 | """ 134 | :return: url of the page redirected to from wpad 135 | """ 136 | wpad_data = self.retry_get('http://wpad.{}'.format(self.info['domain'])) 137 | 138 | if wpad_data.status_code != 200: 139 | self.log.error('Failed to retrieve wpad from {}'.format(self.info['domain'])) 140 | sys.exit() 141 | 142 | so = soup.BeautifulSoup(wpad_data.content, 'lxml') 143 | 144 | self.info['url'] = so.find('meta', attrs={'http-equiv': 'refresh'})['content'].partition('=')[2] 145 | 146 | self.log.info('Found redirect url: {}'.format(self.info['url'])) 147 | 148 | def login(self): 149 | """ 150 | :return: Status code for our post to the login page 151 | """ 152 | logon_page = self.retry_get(self.info['url']) 153 | 154 | so2 = soup.BeautifulSoup(logon_page.content, 'lxml') 155 | 156 | form = so2.find('form') 157 | 158 | post_data = form.find_all('input', attrs={'type': 'hidden'}) 159 | 160 | post_dict = {} 161 | for entry in post_data: 162 | post_dict[entry['name']] = entry['value'] 163 | 164 | self.log.debug('Login form data: {}'.format(post_dict)) 165 | 166 | return requests.post(form['action'], data=post_dict).status_code 167 | 168 | def get_config(self): 169 | try: 170 | self.config = jconf.Jconf('conf.json') 171 | except Exception as e: 172 | sys.exit(e) 173 | 174 | def setup_logging(self): 175 | # Silence request module 176 | logging.getLogger("requests").setLevel(logging.WARNING) 177 | 178 | # Setup log vars 179 | self.log = logging.getLogger('') 180 | 181 | self.format = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s") 182 | 183 | # Main logger 184 | rootlogger = logging.getLogger() 185 | rootlogger.setLevel(logging.DEBUG) 186 | 187 | # Print to console 188 | consolehandler = logging.StreamHandler() 189 | consolehandler.setFormatter(self.format) 190 | rootlogger.addHandler(consolehandler) 191 | 192 | # Good to go 193 | self.log.debug('Logging setup complete') 194 | 195 | 196 | if __name__ == '__main__': 197 | 198 | # Init class 199 | main = RollMac() 200 | 201 | # Loop forever 202 | while True: 203 | main.run() 204 | --------------------------------------------------------------------------------