#!/usr/bin/env python3 # -*- coding: utf-8 -*- import os import sys import json import time import datetime import random import hashlib import subprocess import re import requests import atexit import socket sys.path.append(os.path.dirname(os.path.abspath(__file__))) import ssh2 from ssh2.error_codes import LIBSSH2_ERROR_EAGAIN from ssh2.utils import wait_socket import telnetlib import ftplib if sys.version_info < (3,8,0): print("ERROR: Requires Python v3.8 or higher!") sys.exit(1) from multiprocessing import shared_memory def die(*args): err = 1 prefix = "ERROR: " msg = "" if len(args) > 0: if isinstance(args[0], int): err = args[0] else: msg = args[0] if (err == 0): prefix = "" if len(args) > 1: msg = args[1] print(" ") print(prefix + msg) print(" ") sys.exit(err) def get_http_headers(): headers = {} headers["Content-Type"] = "application/x-www-form-urlencoded; charset=UTF-8" headers["User-Agent"] = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:65.0) Gecko/20100101 Firefox/65.0" return headers class Gateway(): use_ssh = True verbose = 2 timeout = 4 memcfg = None # shared memory "XMiR_12345" device_name = None rom_version = None rom_channel = None mac_address = None nonce_key = None stok = None # HTTP session token status = -2 ftp = None socket = None # TCP socket for SSH ssh = None # SSH session login = 'root' # default username def __init__(self, timeout = 4, verbose = 2, detect_device = True, detect_ssh = True, load_cfg = True): self.verbose = verbose self.timeout = timeout self.device_name = None self.status = -2 self.init_memcfg(load_cfg) atexit.register(self.shutdown) os.makedirs('outdir', exist_ok = True) os.makedirs('tmp', exist_ok = True) if detect_device: self.detect_device() if detect_ssh: verb = 1 if verbose else 0 interact = True if verbose else False port = self.detect_ssh(verbose = 1, interactive = interact) if port <= 0: die("Can't found valid SSH server on IP {}".format(self.ip_addr)) def detect_device(self): self.device_name = None self.rom_version = None self.rom_channel = None self.mac_address = None self.nonce_key = None self.status = -2 try: r0 = requests.get("http://{ip_addr}/cgi-bin/luci/web".format(ip_addr = self.ip_addr), timeout = self.timeout) r0.raise_for_status() #with open("r0.txt", "wb") as file: # file.write(r0.text.encode("utf-8")) hardware = re.findall(r'hardware = \'(.*?)\'', r0.text) if hardware and len(hardware) > 0: self.device_name = hardware[0] else: hardware = re.findall(r'hardwareVersion: \'(.*?)\'', r0.text) if hardware and len(hardware) > 0: self.device_name = hardware[0] self.device_name = self.device_name.upper() romver = re.search(r'romVersion: \'(.*?)\'', r0.text) self.rom_version = romver.group(1).strip() if romver else None romchan = re.search(r'romChannel: \'(.*?)\'', r0.text) self.rom_channel = romchan.group(1).strip().lower() if romchan else None mac_address = re.search(r'var deviceId = \'(.*?)\'', r0.text) self.mac_address = mac_address.group(1) if mac_address else None nonce_key = re.search(r'key: \'(.*)\',', r0.text) self.nonce_key = nonce_key.group(1) if nonce_key else None except requests.exceptions.HTTPError as e: print("Http Error:", e) except requests.exceptions.ConnectionError as e: #print("Error Connecting:", e) return self.status except requests.exceptions.ConnectTimeout as e: print ("ConnectTimeout Error:", e) except requests.exceptions.Timeout as e: print ("Timeout Error:", e) except requests.exceptions.RequestException as e: print("Request Exception:", e) except Exception: pass if not self.device_name: die("You need to make the initial configuration in the WEB of the device!") self.status = -1 x = r0.text.find('a href="/cgi-bin/luci/web/init/hello') if (x > 10): die("You need to make the initial configuration in the WEB of the device!") self.status = 1 return self.status def web_ping(self, timeout, wait_timeout = 0): ret = True start_time = datetime.datetime.now() try: res = requests.get("http://{ip_addr}/cgi-bin/luci/web".format(ip_addr = self.ip_addr), timeout = timeout) res.raise_for_status() mac_address = re.search(r'var deviceId = \'(.*?)\'', res.text) self.mac_address = mac_address.group(1) if mac_address else None nonce_key = re.search(r'key: \'(.*)\',', res.text) self.nonce_key = nonce_key.group(1) if nonce_key else None except Exception: ret = False if wait_timeout > 0: dt = (wait_timeout * 1000 * 1000) - (datetime.datetime.now() - start_time).microseconds if dt > 0: time.sleep(dt / 1000 / 1000) return ret def web_login(self): self.stok = None if not self.nonce_key or not self.mac_address: die("Xiaomi Mi Wi-Fi device is wrong model or not the stock firmware in it.") nonce = "0_" + self.mac_address + "_" + str(int(time.time())) + "_" + str(random.randint(1000, 10000)) web_pass = self.webpassword if not web_pass: web_pass = input("Enter device WEB password: ") account_str = (web_pass + self.nonce_key).encode('utf-8') account_str = hashlib.sha1(account_str).hexdigest() password = (nonce + account_str).encode('utf-8') password = hashlib.sha1(password).hexdigest() username = 'admin' data = "username={username}&password={password}&logtype=2&nonce={nonce}".format(username = username, password = password, nonce = nonce) requrl = "http://{ip_addr}/cgi-bin/luci/api/xqsystem/login".format(ip_addr = self.ip_addr) res = requests.post(requrl, data = data, headers = get_http_headers()) try: stok = re.findall(r'"token":"(.*?)"', res.text)[0] except Exception: self.webpassword = "" die("WEB password is not correct!") self.webpassword = web_pass self.stok = stok return stok @property def apiurl(self): return "http://{ip_addr}/cgi-bin/luci/;stok={stok}/api/".format(ip_addr = self.ip_addr, stok = self.stok) def get_pub_info(self, api_name, timeout = 5): subsys = 'xqsystem' if api_name == 'router_info' or api_name == 'topo_graph': subsys = 'misystem' try: url = "http://{ip_addr}/cgi-bin/luci/api/{subsys}/{api_name}".format(ip_addr = self.ip_addr, subsys = subsys, api_name = api_name) res = requests.get(url, timeout = timeout) res.raise_for_status() except Exception: return {} return json.loads(res.text) def get_init_info(self, timeout = 5): return self.get_pub_info('init_info', timeout = timeout) def get_factory_info(self, timeout = 5): self.facinfo = self.get_pub_info('fac_info', timeout = timeout) return self.facinfo def get_bdata_info(self, timeout = 5): return self.get_pub_info('bdata', timeout = timeout) def get_ip_info(self, timeout = 5): return self.get_pub_info('get_ip', timeout = timeout) def get_upgrade_status(self, timeout = 5): return self.get_pub_info('upgrade_status', timeout = timeout) def get_router_info(self, timeout = 5): return self.get_pub_info('router_info', timeout = timeout) def get_topo_graph_info(self, timeout = 5): return self.get_pub_info('topo_graph', timeout = timeout) def wait_shutdown(self, timeout, verbose = 1): if verbose: print('Waiting for shutdown: ', end='', flush=True) start_time = datetime.datetime.now() while datetime.datetime.now() - start_time <= datetime.timedelta(seconds = timeout): if verbose: print('.', end='', flush=True) if self.web_ping(1, 1) is False: if verbose: print('.', flush=True) time.sleep(1) self.stok = None return True if verbose: print('timedout', flush=True) return False def wait_reboot(self, timeout, verbose = 1): if verbose: print('Waiting for reboot: ', end='', flush=True) start_time = datetime.datetime.now() while datetime.datetime.now() - start_time <= datetime.timedelta(seconds = timeout): if verbose: print('.', end='', flush=True) if self.web_ping(1, 1) is True: if verbose: print('log', end='', flush=True) self.web_login() # TODO if verbose: print('on', end='', flush=True) for i in range(5): time.sleep(1) if verbose: print('.', end='', flush=True) print('', flush=True) return True if verbose: print('timedout', flush=True) return False def reboot_device(self, wait_timeout = None): try: params = { 'client': 'web' } res = requests.post(gw.apiurl + "xqsystem/reboot", params = params, timeout=self.timeout) if res.text.find('"code":0') < 0: return False if wait_timeout: if not self.wait_shutdown(wait_timeout): return False return True except Exception as e: return False #=============================================================================== def shutdown(self): self.ssh_close() try: self.ftp.quit() except Exception: pass try: self.ftp.close() except Exception: pass self.ftp = None #=============================================================================== def init_memcfg(self, load_cfg = True): _env_master_cfg = 'XMiR_cfg' _memcfgname = 'XMiR_' _memcfgsize = 1024*1024 ppid = os.environ[_env_master_cfg] if _env_master_cfg in os.environ else None if ppid is None: load_cfg = False if load_cfg: try: sm = shared_memory.SharedMemory(_memcfgname + ppid) except FileNotFoundError: del os.environ[_env_master_cfg] load_cfg = False if not load_cfg: sm = shared_memory.SharedMemory(_memcfgname + '%d' % os.getpid(), create=True, size=_memcfgsize) if not _env_master_cfg in os.environ: os.environ[_env_master_cfg] = str(os.getpid()) self.memcfg = sm def load_memcfg(self): cfg = {} if not self.memcfg: return cfg size = bytes(self.memcfg.buf[:4]) size = int.from_bytes(size, byteorder='little', signed=True) if size <= 0: return cfg data = bytes(self.memcfg.buf[4:4+size]) cfg = json.loads(data.decode('utf-8')) return cfg def get_memcfg_param(self, key, defvalue = None): cfg = self.load_memcfg() return cfg[key] if key in cfg else defvalue def set_memcfg_param(self, key, value): cfg = self.load_memcfg() cfg[key] = value.strip() if isinstance(value, str) else value self.save_memcfg(cfg) def save_memcfg(self, cfg): data = b'' if cfg: data = json.dumps(cfg, ensure_ascii=False).encode('utf-8') size = len(data) self.memcfg.buf[:4] = size.to_bytes(4, byteorder='little', signed=True) if size > 0: self.memcfg.buf[4:4+len(data)] = data #=============================================================================== @property def ssh_port(self): return self.get_memcfg_param('ssh_port', 22) @ssh_port.setter def ssh_port(self, value): self.set_memcfg_param('ssh_port', value) @property def passw(self): return self.get_memcfg_param('passw', None) # password for root user @passw.setter def passw(self, value): self.set_memcfg_param('passw', value) @property def webpassword(self): return self.get_memcfg_param('webpassword', None) @webpassword.setter def webpassword(self, value): self.set_memcfg_param('webpassword', value) #=============================================================================== @property def ip_addr(self): return self.get_config_param('device_ip_addr', '192.168.1.1').strip() @ip_addr.setter def ip_addr(self, value): self.set_config_param('device_ip_addr', value) #=============================================================================== def load_config(self): config = {} if os.path.exists('config.txt'): with open('config.txt', 'r') as file: config = json.load(file) return config def get_config_param(self, key, defvalue = None): config = self.load_config() return config[key] if key in config else defvalue def set_config_param(self, key, value): config = self.load_config() config[key] = value.strip() if isinstance(value, str) else value self.save_config(config) def save_config(self, config): with open('config.txt', 'w') as file: json.dump(config, file, indent=4, sort_keys=True) #=============================================================================== def check_ssh(self, ip, port, password, contimeout = 2, timeout = 3): err = 0 sock = None ssh = None start_time = datetime.datetime.now() while datetime.datetime.now() - start_time <= datetime.timedelta(seconds = contimeout): try: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.settimeout(0.5) sock.connect((ip, port)) break except Exception as e: sock = None pass if not sock: err = -1 if password and err == 0: try: ssh = ssh2.session.Session() sock.settimeout(timeout) ssh.handshake(sock) except Exception as e: err = -2 if err == 0: try: ssh.userauth_password(self.login, password) except Exception as e: err = -3 try: ssh.disconnect() except Exception: pass try: sock.close() except Exception: pass return err def detect_ssh(self, verbose = 1, interactive = True, contimeout = 2, aux_port = 122): ip_addr = self.ip_addr ssh_port = self.ssh_port if ssh_port == aux_port: aux_port = 22 passw = self.passw if passw: ret = self.check_ssh(ip_addr, ssh_port, passw, contimeout = contimeout) if ret >= 0: return ssh_port # OK if ret == -1: ssh_port = 0 if ssh_port: portlist = [ ssh_port, aux_port ] else: portlist = [ aux_port ] plist = [] for i, port in enumerate(portlist): ret = self.check_ssh(ip_addr, port, None, contimeout = contimeout) if ret == 0: plist.append(port) if not plist: if verbose >= 2: print("Can't found valid SSH server on IP {}".format(ip_addr)) return -1 if passw: pswlist = [ passw ] if passw != 'root': pswlist.append('root') else: pswlist = ['root', None] for p, psw in enumerate(pswlist): if psw is None: if not interactive: continue psw = input("Enter password for root: ") for i, port in enumerate(plist): ret = self.check_ssh(ip_addr, port, psw, contimeout = contimeout) if ret >= 0: self.passw = psw self.ssh_port = port if verbose: print("Detect valid SSH server on port {} (auth OK)".format(port)) return port if ret == -3 and passw and psw == passw: if verbose: print("Set SSH password = None") self.passw = None passw = None if verbose >= 2: print("Can't found valid SSH server on IP {}".format(ip_addr)) return -2 def ssh_close(self): try: self.ssh.disconnect() except Exception: pass try: self.socket.close() except Exception: pass self.ssh = None self.socket = None def set_timeout(self, timeout): self.timeout = timeout if self.use_ssh and self.ssh: self.ssh.set_timeout(int(self.timeout * 1000)) def get_ssh(self, verbose = 0, contimeout = None): if self.ssh: try: self.ssh.keepalive_send() return self.ssh except Exception: pass self.shutdown() try: contimeout = 1 if contimeout is None else contimeout start_time = datetime.datetime.now() while datetime.datetime.now() - start_time <= datetime.timedelta(seconds = contimeout): try: self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.socket.settimeout(0.5) self.socket.connect((self.ip_addr, self.ssh_port)) break except Exception as e: self.socket = None pass if not self.socket: raise Exception('') self.socket.settimeout(None) # enable blocking mode self.ssh = ssh2.session.Session() self.ssh.handshake(self.socket) self.ssh.userauth_password(self.login, self.passw) self.ssh.set_blocking(True) self.ssh.set_timeout(int(self.timeout * 1000)) return self.ssh except Exception as e: #print(e) if verbose: die("SSH server not responding (IP: {})".format(self.ip_addr)) self.shutdown() return None def check_telnet(self, timeout = 2, port = 23, verbose = 0): try: tn = telnetlib.Telnet(self.ip_addr, port=port, timeout=timeout) tn.close() return True except Exception as e: if verbose: die("TELNET not responding (IP: {})".format(self.ip_addr)) return False def get_telnet(self, verbose = 0): try: tn = telnetlib.Telnet(self.ip_addr, timeout=4) except Exception as e: if verbose: die("TELNET not responding (IP: {})".format(self.ip_addr)) return None try: p_login = b'login: ' p_passw = b'Password: ' prompt = "{}@XiaoQiang:(.*?)#".format(self.login).encode('ascii') idx, obj, output = tn.expect([p_login, prompt], timeout=2) if idx < 0: raise Exception('') if idx > 0: tn.prompt = obj.group() return tn tn.write("{}\n".format(self.login).encode('ascii')) idx, obj, output = tn.expect([p_passw, prompt], timeout=2) if idx < 0: raise Exception('') if idx > 0: tn.prompt = obj.group() return tn tn.write("{}\n".format(self.passw).encode('ascii')) idx, obj, output = tn.expect([prompt], timeout=2) if idx < 0: raise Exception('') tn.prompt = obj.group() return tn except Exception as e: #print(e) if verbose: die("Can't login to TELNET (IP: {})".format(self.ip_addr)) return None def get_ftp(self, verbose = 0): if self.ftp and self.ftp.sock: try: self.ftp.voidcmd("NOOP") return self.ftp #Already connected except Exception: pass self.shutdown() try: #timeout = 10 if self.timeout < 10 else self.timeout self.ftp = ftplib.FTP(self.ip_addr, user=self.login, passwd=self.passw, timeout=self.timeout) self.ftp.voidcmd("NOOP") return self.ftp except Exception: if verbose: die("ftp not responding (IP: {})".format(self.ip_addr)) self.shutdown() return None def ping(self, verbose = 2, contimeout = None): if self.use_ssh: ssh = self.get_ssh(verbose, contimeout) if not ssh: return False else: tn = self.get_telnet(verbose) if not tn: return False ftp = self.get_ftp(verbose) if not ftp: return False return True def run_cmd(self, cmd, msg = None, timeout = None, die_on_error = True): ret = True if self.use_ssh: ssh = self.get_ssh(self.verbose) else: tn = self.get_telnet(self.verbose) if (msg): print(msg) cmdlist = [] if isinstance(cmd, str): cmdlist.append(cmd) else: cmdlist = cmd for idx, cmd in enumerate(cmdlist): if self.use_ssh: channel = ssh.open_session() if timeout is not None: saved_timeout = ssh.get_timeout() ssh.set_timeout(int(timeout * 1000)) #channel.pty('xterm') #print("exec = '{}'".format(cmd)) channel.execute(cmd) try: channel.wait_eof() except ssh2.exceptions.Timeout: ssh.set_timeout(100) ret = False if die_on_error: die("SSH execute command timed out! CMD: \"{}\"".format(cmd)) if timeout is not None: ssh.set_timeout(saved_timeout) try: channel.close() channel.wait_closed() except Exception: pass #status = channel.get_exit_status() if not ret: break else: cmd = (cmd + '\n').encode('ascii') tn.write(cmd) tn.read_until(tn.prompt, timeout = 4 if timeout is None else timeout) if not self.use_ssh: tn.write(b"exit\n") return ret def download(self, fn_remote, fn_local, verbose = 1): if verbose and self.verbose: print('Download file: "{}" ....'.format(fn_remote)) if self.use_ssh: ssh = self.get_ssh(self.verbose) channel, fileinfo = ssh.scp_recv2(fn_remote) total_size = fileinfo.st_size read_size = 0 with open(fn_local, 'wb') as file: while read_size < total_size: size, data = channel.read() if size > 0: if read_size + len(data) > total_size: file.write(data[:total_size - read_size]) else: file.write(data) read_size += size else: ftp = self.get_ftp(self.verbose) file = open(fn_local, 'wb') ftp.retrbinary('RETR ' + fn_remote, file.write) file.close() return True def upload(self, fn_local, fn_remote, verbose = 1): try: file = open(fn_local, 'rb') except Exception: die('File "{}" not found.'.format(fn_local)) if verbose and self.verbose: print('Upload file: "{}" ....'.format(fn_local)) if self.use_ssh: ssh = self.get_ssh(self.verbose) finfo = os.stat(fn_local) channel = ssh.scp_send64(fn_remote, finfo.st_mode & 0o777, finfo.st_size, finfo.st_mtime, finfo.st_atime) size = 0 for data in file: channel.write(data) size = size + len(data) #except ssh2.exceptions.SCPProtocolError as e: else: ftp = self.get_ftp(self.verbose) ftp.storbinary('STOR ' + fn_remote, file) file.close() return True #=============================================================================== if __name__ == "__main__": if len(sys.argv) > 1: ip_addr = sys.argv[1] gw = Gateway(detect_device = False, detect_ssh = False) gw.ip_addr = ip_addr print("Device IP-address changed to {}".format(ip_addr))