#!/usr/bin/env python # -*- coding: UTF-8 -*- """ ngrok.cc 内网穿透服务 Python 版 本程序仅适用于ngrok.cc 使用前请先在 https://ngrok.cc 注册账号. Linux 系统一般自带Python 可以直接运行 赋予权限 chmod 755 sunny.py 直接运行 ./sunny.py --clientid=xxxxxxxxxxxxxxxx 命令行模式执行 python sunny.py --clientid=xxxxxx 即可运行 感谢 hauntek 提供的 python-ngrok 原版程序 """ import getopt import socket import ssl import json import struct import random import sys import time import logging import threading python_version = sys.version_info >= (3, 0) if not python_version: reload(sys) sys.setdefaultencoding('utf8') options = { 'clientid':'', } def usage(): print( ' -h help \n' \ ' -a clientid xxxxxxxxxxxxxxxx\n' \ ) sys.exit() try: opts, args = getopt.getopt(sys.argv[1:], "h:c:", ['help', "clientid="]) except getopt.GetoptError: usage() if len(opts) == 0: print( '使用说明\n' \ '在命令行模式运行 python sunny.py --clientid=xxxxxxxx\n' \ '如果是多个隧道换成 python sunny.py --clientid=xxxxxxxx,xxxxxxxx\n' \ '请登录 https://ngrok.cc 获取 clientid\n' \ ) for option, value in opts: if option in ['-h', '--help']: usage() if option in ['-c', '--clientid']: options['clientid'] = value if options['clientid'] == '': if not python_version: input_clientid = raw_input('请输入clientid:') else: input_clientid = str(input('请输入clientid:')) if input_clientid != '': options['clientid'] = input_clientid else: sys.exit() Tunnels = list() # 全局渠道赋值 reqIdaddr = dict() localaddr = dict() # ngrok.cc 添加到渠道队列 def ngrok_adds(Tunnel): global Tunnels for tunnelinfo in Tunnel: if tunnelinfo.get('proto'): if tunnelinfo.get('proto').get('http'): protocol = 'http' if tunnelinfo.get('proto').get('https'): protocol = 'https' if tunnelinfo.get('proto').get('tcp'): protocol = 'tcp' proto = tunnelinfo['proto'][protocol].split(':') # 127.0.0.1:80 拆分成数组 if proto[0] == '': proto[0] = '127.0.0.1' if proto[1] == '' or proto[1] == 0: proto[1] = 80 body = dict() body['protocol'] = protocol body['hostname'] = tunnelinfo['hostname'] body['subdomain'] = tunnelinfo['subdomain'] body['httpauth'] = tunnelinfo['httpauth'] body['rport'] = tunnelinfo['remoteport'] body['lhost'] = str(proto[0]) body['lport'] = int(proto[1]) Tunnels.append(body) # 加入渠道队列 # ngrok.cc 获取服务器设置 def ngrok_auth(options): host = 'www.ngrok.cc' port = 443 try: client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ssl_client = ssl.wrap_socket(client, ssl_version=ssl.PROTOCOL_TLSv1_2) # ssl.PROTOCOL_TLSv1_2 ssl_client.connect((host, port)) except Exception: print('连接认证服务器: https://www.ngrok.cc 错误.') time.sleep(10) sys.exit() header = "POST " + "/api/clientid/clientid/%s" + " HTTP/1.1" + "\r\n" header += "Content-Type: text/html" + "\r\n" header += "Host: %s" + "\r\n" header += "\r\n" buf = header % (options, host) ssl_client.sendall(buf.encode('utf-8')) # 发送请求头 fd = ssl_client.makefile('rb', 0) body = bytes() while True: line = fd.readline().decode('utf-8') if line == "\n" or line == "\r\n": chunk_size = int(fd.readline(), 16) if chunk_size > 0: body = fd.read(chunk_size).decode('utf-8') break ssl_client.close() authData = json.loads(body) if authData['status'] != 200: print('认证错误:%s, ErrorCode:%s' % (authData['msg'], authData['status'])) time.sleep(10) sys.exit() print('认证成功,正在连接服务器...') # 设置映射隧道,支持多渠道[客户端id] ngrok_adds(authData['data']) proto = authData['server'].split(':') return proto print('欢迎使用内网穿透 python-ngrok v1.42\r\nCtrl+C 退出') serverArr = ngrok_auth(options['clientid']) host = str(serverArr[0]) # Ngrok服务器地址 port = int(serverArr[1]) # 端口 bufsize = 1024 # 吞吐量 mainsocket = 0 ClientId = '' pingtime = 0 def getloacladdr(Tunnels, Url): protocol = Url[0:Url.find(':')] hostname = Url[Url.find('//') + 2:] subdomain = hostname[0:hostname.find('.')] rport = Url[Url.rfind(':') + 1:] for tunnelinfo in Tunnels: if tunnelinfo.get('protocol') == protocol: if tunnelinfo.get('protocol') in ['http', 'https']: if tunnelinfo.get('hostname') == hostname: return tunnelinfo if tunnelinfo.get('subdomain') == subdomain: return tunnelinfo if tunnelinfo.get('protocol') == 'tcp': if tunnelinfo.get('rport') == int(rport): return tunnelinfo return dict() def dnsopen(host): try: ip = socket.gethostbyname(host) except socket.error: return False return ip def connectremote(host, port): try: host = socket.gethostbyname(host) client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) ssl_client = ssl.wrap_socket(client, ssl_version=ssl.PROTOCOL_SSLv23) ssl_client.connect((host, port)) ssl_client.setblocking(1) logger = logging.getLogger('%s:%d' % ('Conn', ssl_client.fileno())) logger.debug('New connection to: %s:%d' % (host, port)) except socket.error: return False return ssl_client def connectlocal(localhost, localport): try: localhost = socket.gethostbyname(localhost) client = socket.socket(socket.AF_INET, socket.SOCK_STREAM) client.connect((localhost, localport)) client.setblocking(1) logger = logging.getLogger('%s:%d' % ('Conn', client.fileno())) logger.debug('New connection to: %s:%d' % (localhost, localport)) except socket.error: return False return client def NgrokAuth(): Payload = dict() Payload['ClientId'] = '' Payload['OS'] = 'darwin' Payload['Arch'] = 'amd64' Payload['Version'] = '2' Payload['MmVersion'] = '2.1' Payload['User'] = 'user' Payload['Password'] = '' body = dict() body['Type'] = 'Auth' body['Payload'] = Payload buffer = json.dumps(body) return(buffer) def ReqTunnel(ReqId, Protocol, Hostname, Subdomain, HttpAuth, RemotePort): Payload = dict() Payload['ReqId'] = ReqId Payload['Protocol'] = Protocol Payload['Hostname'] = Hostname Payload['Subdomain'] = Subdomain Payload['HttpAuth'] = HttpAuth Payload['RemotePort'] = RemotePort body = dict() body['Type'] = 'ReqTunnel' body['Payload'] = Payload buffer = json.dumps(body) return(buffer) def RegProxy(ClientId): Payload = dict() Payload['ClientId'] = ClientId body = dict() body['Type'] = 'RegProxy' body['Payload'] = Payload buffer = json.dumps(body) return(buffer) def Ping(): Payload = dict() body = dict() body['Type'] = 'Ping' body['Payload'] = Payload buffer = json.dumps(body) return(buffer) def lentobyte(len): return struct.pack(' 0: if not recvbuf: recvbuf = recvbut else: recvbuf += recvbut if type == 1 or (type == 2 and linkstate == 1): lenbyte = tolen(recvbuf[0:8]) if len(recvbuf) >= (8 + lenbyte): buf = recvbuf[8:lenbyte + 8].decode('utf-8') logger = logging.getLogger('%s:%d' % ('Recv', sock.fileno())) logger.debug('Reading message with length: %d' % len(buf)) logger.debug('Read message: %s' % buf) js = json.loads(buf) if type == 1: if js['Type'] == 'ReqProxy': newsock = connectremote(host, port) if newsock: thread = threading.Thread(target = HKClient, args = (newsock, 0, 2)) thread.setDaemon(True) thread.start() if js['Type'] == 'AuthResp': ClientId = js['Payload']['ClientId'] logger = logging.getLogger('%s' % 'client') logger.debug('Authenticated with server, client id: %s' % ClientId) sendpack(sock, Ping()) pingtime = time.time() for info in Tunnels: reqid = getRandChar(8) sendpack(sock, ReqTunnel(reqid, info['protocol'], info['hostname'], info['subdomain'], info['httpauth'], info['rport'])) reqIdaddr[reqid] = (info['lhost'], info['lport']) if js['Type'] == 'NewTunnel': if js['Payload']['Error'] != '': logger = logging.getLogger('%s' % 'client') logger.error('Server failed to allocate tunnel: %s' % js['Payload']['Error']) print('隧道建立失败: %s' % js['Payload']['Error']) time.sleep(30) else: logger = logging.getLogger('%s' % 'client') logger.debug('Tunnel established at %s' % js['Payload']['Url']) print('隧道建立成功: %s' % js['Payload']['Url']) # 注册成功 localaddr[js['Payload']['Url']] = reqIdaddr[js['Payload']['ReqId']] if type == 2: if js['Type'] == 'StartProxy': localhost, localport = localaddr[js['Payload']['Url']] newsock = connectlocal(localhost, localport) if newsock: thread = threading.Thread(target = HKClient, args = (newsock, 0, 3, sock)) thread.setDaemon(True) thread.start() tosock = newsock linkstate = 2 else: body = 'Web服务错误
隧道 %s 无效
无法连接到%s. 此端口尚未提供Web服务
' html = body % (js['Payload']['Url'], localhost + ':' + str(localport)) header = "HTTP/1.0 502 Bad Gateway" + "\r\n" header += "Content-Type: text/html" + "\r\n" header += "Content-Length: %d" + "\r\n" header += "\r\n" + "%s" buf = header % (len(html.encode('utf-8')), html) sendbuf(sock, buf.encode('utf-8')) if len(recvbuf) == (8 + lenbyte): recvbuf = bytes() else: recvbuf = recvbuf[8 + lenbyte:] if type == 3 or (type == 2 and linkstate == 2): sendbuf(tosock, recvbuf) recvbuf = bytes() except socket.error: break if type == 1: mainsocket = False if type == 3: try: tosock.shutdown(socket.SHUT_WR) except socket.error: tosock.close() logger = logging.getLogger('%s:%d' % ('Close', sock.fileno())) logger.debug('Closing') sock.close() # 客户端程序初始化 if __name__ == '__main__': logging.basicConfig(level=logging.INFO, format='[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s', datefmt='%Y/%m/%d %H:%M:%S') logger = logging.getLogger('%s' % 'client') logger.debug('python-ngrok v1.5') while True: try: # 检测控制连接是否连接. if mainsocket == False: ip = dnsopen(host) if ip == False: logger = logging.getLogger('%s' % 'client') logger.debug('update dns') print('连接ngrok服务器失败.') time.sleep(10) continue mainsocket = connectremote(ip, port) if mainsocket == False: logger = logging.getLogger('%s' % 'client') logger.debug('connect failed...!') print('连接ngrok服务器失败.') time.sleep(10) continue thread = threading.Thread(target = HKClient, args = (mainsocket, 0, 1)) thread.setDaemon(True) thread.start() # 发送心跳 if pingtime + 20 < time.time() and pingtime != 0: sendpack(mainsocket, Ping()) pingtime = time.time() time.sleep(1) except socket.error: pingtime = 0 except KeyboardInterrupt: sys.exit()