├── .gitignore ├── BaseRouter.py ├── README.md ├── TPLinkRouter.py ├── XiaomiRouter.py ├── config ├── RouterConfig.py ├── __init__.py ├── configCommon.py ├── logger.py └── urlConf.py ├── httpUtils.py └── run.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.html 2 | *.pyc 3 | *.yaml 4 | *.log 5 | .idea/ 6 | -------------------------------------------------------------------------------- /BaseRouter.py: -------------------------------------------------------------------------------- 1 | from httpUtils import HTTPClient 2 | 3 | 4 | class baseRouter: 5 | def __init__(self): 6 | self.httpClient = HTTPClient(0) 7 | 8 | def login(self, RouterPwd): 9 | """ 10 | 登录 11 | :param RouterPwd: 12 | :return: 13 | """ 14 | pass 15 | 16 | def disconnect(self): 17 | """ 18 | 断开路由 19 | :return: 20 | """ 21 | pass 22 | 23 | def connect(self, broadbandUserName, broadbandPwd): 24 | """ 25 | 启动路由 26 | :return: 27 | """ 28 | pass 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | #### 自动重启路由,更换出口ip 2 | 3 | ps: 目前只支持小米和TPLINK -------------------------------------------------------------------------------- /TPLinkRouter.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | import time 4 | import execjs 5 | from BaseRouter import baseRouter 6 | from config import RouterConfig 7 | from config.urlConf import urls 8 | 9 | 10 | class tpLinkRouter(baseRouter): 11 | def __init__(self): 12 | super().__init__() 13 | self.stok = None 14 | 15 | def secretJs(self, pwd): 16 | """ 17 | TPLINK加密登录密码 18 | :return: 19 | """ 20 | jsStr = """ 21 | function a() { 22 | a = '""" + pwd + """'; 23 | c = 'RDpbLfCPsJZ7fiv'; 24 | b = 'yLwVl0zKqws7LgKPRQ84Mdt708T1qQ3Ha7xv3H7NyU84p21BriUWBU43odz3iP4rBL3cD02KZciXTysVXiV8ngg6vL48rPJyAUw0HurW20xqxv9aYb4M9wK1Ae0wlro510qXeU07kV57fQMc8L6aLgMLwygtc0F10a0Dg70TOoouyFhdysuRMO51yY5ZlOZZLEal1h0t9YQW0Ko7oBwmCAHoic4HYbUyVeU3sfQ1xtXcPcf1aT303wAQhv66qzW' 25 | var d = '', 26 | e, f, g, h, k = 187, 27 | m = 187; 28 | f = a.length; 29 | g = c.length; 30 | h = b.length; 31 | e = f > g ? f : g; 32 | for (var l = 0; l < e; l++) m = k = 187, l >= f ? m = c.charCodeAt(l) : l >= g ? k = a.charCodeAt(l) : (k = a.charCodeAt(l), m = c.charCodeAt(l)), d += b.charAt((k ^ m) % h); 33 | return d 34 | } 35 | """ 36 | jsStrC = execjs.compile(jsStr) 37 | return jsStrC.call("a") 38 | 39 | def login(self, RouterPwd): 40 | """ 41 | 登录 42 | :param RouterPwd: 43 | :return: 44 | """ 45 | secretRouterPwd = self.secretJs(RouterPwd) 46 | TPLinkRouterUrls = urls.get("TPLINK") 47 | data = { 48 | "method": "do", 49 | "login": { 50 | "password": secretRouterPwd 51 | } 52 | } 53 | loginRsp = self.httpClient.send(urls=TPLinkRouterUrls, data=json.dumps(data)) 54 | if loginRsp.get("error_code") is 0: 55 | print("路由器登录成功") 56 | self.stok = loginRsp.get("stok") 57 | else: 58 | print("TP_LINK路由器登录失败:{}".format(loginRsp)) 59 | 60 | def disconnect(self): 61 | if self.stok: 62 | data = { 63 | "network": { 64 | "change_wan_status": { 65 | "proto": "pppoe", 66 | "operate": "disconnect" 67 | } 68 | }, 69 | "method": "do" 70 | } 71 | disconnectUrl = copy.deepcopy(urls["TPds"]) 72 | disconnectUrl["req_url"] = disconnectUrl["req_url"].format(self.stok) 73 | disconnectRsp = self.httpClient.send(urls=disconnectUrl, data=json.dumps(data)) 74 | if disconnectRsp.get("error_code") is 0: 75 | print("断开连接成功") 76 | else: 77 | print("断开路由器失败: {}".format(disconnectRsp)) 78 | 79 | def connect(self, broadbandUserName, broadbandPwd): 80 | if broadbandUserName and broadbandPwd: 81 | wanData = { 82 | "protocol": { 83 | "wan": { 84 | "wan_type": "pppoe" 85 | }, 86 | "pppoe": { 87 | "username": broadbandUserName, 88 | "password": broadbandPwd 89 | } 90 | }, 91 | "method": "set" 92 | } 93 | 94 | connectData = { 95 | "network": { 96 | "change_wan_status": { 97 | "proto": "pppoe", 98 | "operate": "connect" 99 | } 100 | }, 101 | "method": "do" 102 | } 103 | wanStatusData = { 104 | "network": { 105 | "name": ["wan_status"] 106 | }, 107 | "method": "get" 108 | } 109 | connectUrl = copy.deepcopy(urls["TPds"]) 110 | connectUrl["req_url"] = connectUrl["req_url"].format(self.stok) 111 | wanRsp = self.httpClient.send(urls=connectUrl, data=json.dumps(wanData)) 112 | if wanRsp.get("error_code") is 0: 113 | connectRsp = self.httpClient.send(urls=connectUrl, data=json.dumps(connectData)) 114 | if connectRsp.get("error_code") is 0: 115 | for i in range(20): 116 | wanStatusRsp = self.httpClient.send(urls=connectUrl, data=json.dumps(wanStatusData)) 117 | if wanStatusRsp.get("error_code") is 0 and wanStatusRsp.get("network", {}).get("wan_status", {}).get("ipaddr") != "0.0.0.0": 118 | print("宽带重新连接成功,当前更换出口地址为: {}".format(wanStatusRsp.get("network", {}).get("wan_status", {}).get("ipaddr"))) 119 | break 120 | else: 121 | time.sleep(0.5) 122 | else: 123 | print("连接失败: {}".format(wanRsp)) 124 | else: 125 | print("拨号失败: {}".format(wanRsp)) 126 | else: 127 | print("宽带账号密码不能为空") 128 | 129 | def main(self): 130 | while True: 131 | self.login(RouterConfig.routerPwd) 132 | self.disconnect() 133 | self.connect(RouterConfig.broadbandUserName, RouterConfig.broadbandPwd) 134 | time.sleep(RouterConfig.routerTime * 60) 135 | 136 | 137 | if __name__ == '__main__': 138 | tp = tpLinkRouter() 139 | tp.main() 140 | -------------------------------------------------------------------------------- /XiaomiRouter.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import random 3 | import re 4 | import time 5 | 6 | from Crypto.Hash import SHA 7 | 8 | from BaseRouter import baseRouter 9 | from config import RouterConfig 10 | from config.urlConf import urls 11 | 12 | 13 | class xiaomiRouter(baseRouter): 14 | def __init__(self): 15 | super().__init__() 16 | self.stok = None 17 | 18 | def getDeviceId(self): 19 | """ 20 | 获取设备唯一表示 21 | :return: 22 | """ 23 | homeRsp = self.httpClient.send(urls=urls.get("xiaomiHome")) 24 | deviceId = re.findall(r'deviceId = \'(.*)\';', homeRsp)[0] 25 | return deviceId 26 | 27 | def secretJs(self, password): 28 | """ 29 | TPLINK加密登录密码 30 | :return: 31 | """ 32 | key = 'a2ffa5c9be07488bbb04a3a47d3c5f6a' 33 | mac = self.getDeviceId() 34 | nonce = "0_" + mac + "_" + str(int(time.time())) + "_" + str(random.randint(1000, 10000)) 35 | pwd = SHA.new() 36 | pwd.update(password.encode("utf8") + key.encode("utf8")) 37 | hexpwd1 = pwd.hexdigest() 38 | 39 | pwd2 = SHA.new() 40 | pwd2.update(nonce.encode("utf8") + hexpwd1.encode("utf8")) 41 | hexpwd2 = pwd2.hexdigest() 42 | return hexpwd2, nonce 43 | 44 | def login(self, RouterPwd): 45 | """ 46 | 登录 47 | :param RouterPwd: 48 | :return: 49 | """ 50 | secretRouterPwd, nonce = self.secretJs(RouterPwd) 51 | TPLinkRouterUrls = urls.get("xiaomi") 52 | data = { 53 | "username": "admin", 54 | "password": secretRouterPwd, 55 | "logtype": 2, 56 | "nonce": nonce, 57 | } 58 | loginRsp = self.httpClient.send(urls=TPLinkRouterUrls, data=data) 59 | if loginRsp.get("code") is 0: 60 | print("路由器登录成功") 61 | self.stok = loginRsp.get("url").split("web/")[0] 62 | else: 63 | print("小米路由器登录失败:{}".format(loginRsp)) 64 | 65 | def disconnect(self): 66 | if self.stok: 67 | disconnectUrl = copy.deepcopy(urls["xiaomi"]) 68 | disconnectUrl["req_url"] = self.stok + "api/xqnetwork/pppoe_stop" 69 | disconnectRsp = self.httpClient.send(urls=disconnectUrl) 70 | if disconnectRsp.get("code") is 0: 71 | print("断开连接成功") 72 | else: 73 | print("断开路由器失败: {}".format(disconnectRsp)) 74 | 75 | def connect(self, broadbandUserName=None, broadbandPwd=None): 76 | connectUrl = copy.deepcopy(urls["xiaomi"]) 77 | connectUrl["req_url"] = self.stok + "/api/xqnetwork/pppoe_start" 78 | connectRsp = self.httpClient.send(connectUrl) 79 | if connectRsp.get("code") is 0: 80 | statusUrl = copy.deepcopy(urls["xiaomi"]) 81 | statusUrl["req_url"] = self.stok + "/api/xqnetwork/pppoe_status" 82 | statusRsp = "" 83 | for i in range(10): 84 | statusRsp = self.httpClient.send(statusUrl) 85 | address = statusRsp.get("ip", {}).get("address") 86 | if statusRsp.get("code") is 0 and address: 87 | print("路由器重新拨号成功, iP地址为: {}".format(address)) 88 | break 89 | else: 90 | time.sleep(0.5) 91 | else: 92 | print("路由器拨号失败: {}".format(statusRsp)) 93 | else: 94 | print("拨号失败: {}".format(connectRsp)) 95 | 96 | def main(self): 97 | while True: 98 | self.login(RouterConfig.routerPwd) 99 | self.disconnect() 100 | self.connect(RouterConfig.broadbandUserName, RouterConfig.broadbandPwd) 101 | time.sleep(RouterConfig.routerTime * 60) 102 | 103 | 104 | if __name__ == '__main__': 105 | xm = xiaomiRouter() 106 | xm.login("") 107 | xm.disconnect() 108 | xm.connect() 109 | -------------------------------------------------------------------------------- /config/RouterConfig.py: -------------------------------------------------------------------------------- 1 | # 目前只支持TPLINK和小米 2 | # ex: TPLINK or xiaomi 3 | routerType = "" 4 | 5 | # 路由器登录密码 6 | routerPwd = "" 7 | 8 | # 宽带账号账号,小米路由器经测试是不要宽带的账号和密码的 9 | broadbandUserName = "" 10 | 11 | # 宽带密码 12 | broadbandPwd = "" 13 | 14 | # 自动切换ip的时间, 以分钟为单位 15 | routerTime = 20 16 | -------------------------------------------------------------------------------- /config/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/12306lea/AutoRouterIP/fb113d632c161a45c86614675e4202104d73f858/config/__init__.py -------------------------------------------------------------------------------- /config/configCommon.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | import os 4 | import random 5 | import time 6 | 7 | 8 | def getNowTimestamp(): 9 | return time.time() 10 | 11 | 12 | def decMakeDir(func): 13 | def handleFunc(*args, **kwargs): 14 | dirname = func(*args, **kwargs) 15 | if not os.path.exists(dirname): 16 | os.makedirs(dirname) 17 | elif not os.path.isdir(dirname): 18 | pass 19 | 20 | return dirname 21 | 22 | return func 23 | 24 | 25 | def getWorkDir(): 26 | return os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 27 | 28 | # 29 | # def fileOpen(path): 30 | # """ 31 | # 文件读取兼容2和3 32 | # :param path: 文件读取路径 33 | # :return: 34 | # """ 35 | # try: 36 | # with open(path, "r", ) as f: 37 | # return f 38 | # except TypeError: 39 | # with open(path, "r", ) as f: 40 | # return f 41 | 42 | 43 | 44 | @decMakeDir 45 | def getTmpDir(): 46 | return os.path.join(getWorkDir(), "tmp") 47 | 48 | 49 | @decMakeDir 50 | def getLogDir(): 51 | return os.path.join(getTmpDir(), "log") 52 | 53 | 54 | @decMakeDir 55 | def getCacheDir(): 56 | return os.path.join(getTmpDir(), "cache") 57 | 58 | 59 | @decMakeDir 60 | def getVCodeDir(): 61 | return os.path.join(getTmpDir(), "vcode") 62 | 63 | 64 | def getVCodeImageFile(imageName): 65 | return os.path.join(getVCodeDir(), imageName + ".jpg") 66 | 67 | 68 | def getCacheFile(cacheType): 69 | return os.path.join(getCacheDir(), cacheType + ".cache") -------------------------------------------------------------------------------- /config/logger.py: -------------------------------------------------------------------------------- 1 | #coding: utf-8 2 | 3 | import os 4 | import time 5 | import logging 6 | 7 | 8 | logger = None 9 | loggerHandler = None 10 | dateStr = '' #默认拥有日期后缀 11 | suffix = '' #除了日期外的后缀 12 | 13 | def setSuffix(s): 14 | global suffix 15 | suffix = s 16 | 17 | def getTodayDateStr(): 18 | return time.strftime("%Y-%m-%d", time.localtime(configCommon.getNowTimestamp())) 19 | 20 | def setDateStr(s): 21 | global dateStr 22 | dateStr = s 23 | 24 | def isAnotherDay(s): 25 | global dateStr 26 | return dateStr != s 27 | 28 | def getLogFile(): 29 | global dateStr, suffix 30 | rtn = os.path.join(configCommon.getLogDir(), dateStr) 31 | if suffix: 32 | rtn += "_" + suffix 33 | return rtn + ".log" 34 | 35 | def log(msg, func = "info"): 36 | global logger 37 | if not logger: 38 | logger = logging.getLogger() 39 | logger.setLevel(logging.INFO) 40 | 41 | todayStr = getTodayDateStr() 42 | if isAnotherDay(todayStr): 43 | setDateStr(todayStr) 44 | logger.removeHandler(loggerHandler) 45 | 46 | fh = logging.FileHandler(getLogFile()) 47 | fm = logging.Formatter(u'[%(asctime)s][%(levelname)8s] --- %(message)s (%(filename)s:%(lineno)s)') 48 | fh.setFormatter(fm) 49 | 50 | logger.addHandler(fh) 51 | 52 | levels = { 53 | "debug": logger.debug, 54 | "info": logger.info, 55 | "warning": logger.warning, 56 | "error": logger.error, 57 | "critical": logger.critical 58 | } 59 | 60 | levels[func](msg) -------------------------------------------------------------------------------- /config/urlConf.py: -------------------------------------------------------------------------------- 1 | # coding=utf-8 2 | import random 3 | 4 | import time 5 | 6 | urls = { 7 | "TPLINK": { # TPLINK请求地址 8 | "req_url": "/", 9 | "req_type": "post", 10 | "Referer": "", 11 | "Content-Type": 1, 12 | "Host": "192.168.0.1", 13 | "re_try": 10, 14 | "re_time": 0.01, 15 | "s_time": 0.1, 16 | "is_logger": False, 17 | "is_json": True, 18 | "httpType": "http" 19 | }, 20 | "TPds": { # TPLINK路由器内部切换地址 21 | "req_url": "/stok={}/ds", 22 | "req_type": "post", 23 | "Referer": "", 24 | "Content-Type": 1, 25 | "Host": "192.168.0.1", 26 | "re_try": 10, 27 | "re_time": 0.01, 28 | "s_time": 0.1, 29 | "is_logger": False, 30 | "is_json": True, 31 | "httpType": "http" 32 | }, 33 | "xiaomiHome": { # 小米路由器登录 34 | "req_url": "/cgi-bin/luci/web", 35 | "req_type": "get", 36 | "Referer": "", 37 | "Content-Type": 1, 38 | "Host": "192.168.31.1", 39 | "re_try": 10, 40 | "re_time": 0.01, 41 | "s_time": 0.1, 42 | "is_logger": False, 43 | "is_json": False, 44 | "httpType": "http" 45 | }, 46 | "xiaomi": { # 小米路由器登录 47 | "req_url": "/cgi-bin/luci/api/xqsystem/login", 48 | "req_type": "post", 49 | "Referer": "", 50 | "Content-Type": 1, 51 | "Host": "192.168.31.1", 52 | "re_try": 10, 53 | "re_time": 0.01, 54 | "s_time": 0.1, 55 | "is_logger": False, 56 | "is_json": True, 57 | "httpType": "http" 58 | }, 59 | } -------------------------------------------------------------------------------- /httpUtils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf8 -*- 2 | import json 3 | import socket 4 | from collections import OrderedDict 5 | from time import sleep 6 | import requests 7 | 8 | from config import logger 9 | 10 | 11 | def _set_header_default(): 12 | header_dict = OrderedDict() 13 | # header_dict["Accept"] = "application/json, text/plain, */*" 14 | header_dict["Accept-Encoding"] = "gzip, deflate" 15 | header_dict[ 16 | "User-Agent"] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_4) AppleWebKit/537.36 (KHTML, like Gecko) 12306-electron/1.0.1 Chrome/59.0.3071.115 Electron/1.8.4 Safari/537.36" 17 | header_dict["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8" 18 | return header_dict 19 | 20 | 21 | class HTTPClient(object): 22 | 23 | def __init__(self, is_proxy): 24 | """ 25 | :param method: 26 | :param headers: Must be a dict. Such as headers={'Content_Type':'text/html'} 27 | """ 28 | self.initS() 29 | self._cdn = None 30 | self._proxies = None 31 | 32 | def initS(self): 33 | self._s = requests.Session() 34 | self._s.headers.update(_set_header_default()) 35 | return self 36 | 37 | def set_cookies(self, **kwargs): 38 | """ 39 | 设置cookies 40 | :param kwargs: 41 | :return: 42 | """ 43 | for k, v in kwargs.items(): 44 | self._s.cookies.set(k, v) 45 | 46 | def get_cookies(self): 47 | """ 48 | 获取cookies 49 | :return: 50 | """ 51 | return self._s.cookies.values() 52 | 53 | def del_cookies(self): 54 | """ 55 | 删除所有的key 56 | :return: 57 | """ 58 | self._s.cookies.clear() 59 | 60 | def del_cookies_by_key(self, key): 61 | """ 62 | 删除指定key的session 63 | :return: 64 | """ 65 | self._s.cookies.set(key, None) 66 | 67 | def setHeaders(self, headers): 68 | self._s.headers.update(headers) 69 | return self 70 | 71 | def resetHeaders(self): 72 | self._s.headers.clear() 73 | self._s.headers.update(_set_header_default()) 74 | 75 | def getHeadersHost(self): 76 | return self._s.headers["Host"] 77 | 78 | def setHeadersHost(self, host): 79 | self._s.headers.update({"Host": host}) 80 | return self 81 | 82 | def getHeadersReferer(self): 83 | return self._s.headers["Referer"] 84 | 85 | def setHeadersReferer(self, referer): 86 | self._s.headers.update({"Referer": referer}) 87 | return self 88 | 89 | @property 90 | def cdn(self): 91 | return self._cdn 92 | 93 | @cdn.setter 94 | def cdn(self, cdn): 95 | self._cdn = cdn 96 | 97 | def send(self, urls, data=None, **kwargs): 98 | """send request to url.If response 200,return response, else return None.""" 99 | allow_redirects = False 100 | is_logger = urls.get("is_logger", False) 101 | req_url = urls.get("req_url", "") 102 | re_try = urls.get("re_try", 0) 103 | s_time = urls.get("s_time", 0) 104 | is_cdn = urls.get("is_cdn", False) 105 | is_test_cdn = urls.get("is_test_cdn", False) 106 | error_data = {"code": 99999, "message": u"重试次数达到上限"} 107 | if data: 108 | method = "post" 109 | self.setHeaders({"Content-Length": "{0}".format(len(data))}) 110 | else: 111 | method = "get" 112 | self.resetHeaders() 113 | self.setHeadersReferer(urls["Referer"]) 114 | if is_logger: 115 | logger.log( 116 | u"url: {0}\n入参: {1}\n请求方式: {2}\n".format(req_url, data, method, )) 117 | self.setHeadersHost(urls["Host"]) 118 | if is_test_cdn: 119 | url_host = self._cdn 120 | elif is_cdn: 121 | if self._cdn: 122 | # print(u"当前请求cdn为{}".format(self._cdn)) 123 | url_host = self._cdn 124 | else: 125 | url_host = urls["Host"] 126 | else: 127 | url_host = urls["Host"] 128 | http = urls.get("httpType") or "https" 129 | for i in range(re_try): 130 | try: 131 | # sleep(urls["s_time"]) if "s_time" in urls else sleep(0.001) 132 | sleep(s_time) 133 | try: 134 | requests.packages.urllib3.disable_warnings() 135 | except: 136 | pass 137 | response = self._s.request(method=method, 138 | timeout=2, 139 | proxies=self._proxies, 140 | url=http + "://" + url_host + req_url, 141 | data=data, 142 | allow_redirects=allow_redirects, 143 | verify=False, 144 | **kwargs) 145 | if response.status_code == 200: 146 | if urls.get("not_decode", False): 147 | return response.content 148 | if response.content: 149 | if is_logger: 150 | logger.log( 151 | u"出参:{0}".format(response.content)) 152 | if urls["is_json"]: 153 | return json.loads(response.content.decode() if isinstance(response.content, bytes) else response.content) 154 | else: 155 | return response.content.decode("utf8", "ignore") if isinstance(response.content, bytes) else response.content 156 | else: 157 | logger.log( 158 | u"url: {} 返回参数为空".format(urls["req_url"])) 159 | return error_data 160 | else: 161 | sleep(urls["re_time"]) 162 | except (requests.exceptions.Timeout, requests.exceptions.ReadTimeout, requests.exceptions.ConnectionError): 163 | pass 164 | except socket.error: 165 | pass 166 | return error_data 167 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | from TPLinkRouter import tpLinkRouter 2 | from XiaomiRouter import xiaomiRouter 3 | from config import RouterConfig 4 | 5 | if __name__ == '__main__': 6 | if RouterConfig.routerType == "TPLINK": 7 | tp = tpLinkRouter() 8 | tp.main() 9 | elif RouterConfig.routerType == "xiaomi": 10 | xiaomi = xiaomiRouter() 11 | xiaomi.main() --------------------------------------------------------------------------------