PoetryChat / sunny.py
Tsumugii24
initial commit
f7161fa
#!/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('<LL', len, 0)
def sendbuf(sock, buf, isblock = False):
if isblock:
sock.setblocking(1)
sock.sendall(buf)
if isblock:
sock.setblocking(0)
def sendpack(sock, msg, isblock = False):
if isblock:
sock.setblocking(1)
sock.sendall(lentobyte(len(msg)) + msg.encode('utf-8'))
logger = logging.getLogger('%s:%d' % ('Send', sock.fileno()))
logger.debug('Writing message: %s' % msg)
if isblock:
sock.setblocking(0)
def tolen(v):
if len(v) == 8:
return struct.unpack('<II', v)[0]
return 0
def getRandChar(length):
_chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789abcdefghijklmnopqrstuvwxyz"
return ''.join(random.sample(_chars, length))
# 客户端程序处理过程
def HKClient(sock, linkstate, type, tosock = None):
global mainsocket
global ClientId
global pingtime
global reqIdaddr
global localaddr
recvbuf = bytes()
while True:
try:
if linkstate == 0:
if type == 1:
sendpack(sock, NgrokAuth(), False)
linkstate = 1
if type == 2:
sendpack(sock, RegProxy(ClientId), False)
linkstate = 1
if type == 3:
linkstate = 1
recvbut = sock.recv(bufsize)
if not recvbut: break
if len(recvbut) > 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 = '<!DOCTYPE html><html><head><meta charset="utf-8"><title>Web服务错误</title><meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no"><meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"><style>html,body{height:100%%}body{margin:0;padding:0;width:100%%;display:table;font-weight:100;font-family:"Microsoft YaHei",Arial,Helvetica,sans-serif}.container{text-align:center;display:table-cell;vertical-align:middle}.content{border:1px solid #ebccd1;text-align:center;display:inline-block;background-color:#f2dede;color:#a94442;padding:30px}.title{font-size:18px}.copyright{margin-top:30px;text-align:right;color:#000}</style></head><body><div class="container"><div class="content"><div class="title">隧道 %s 无效<br>无法连接到<strong>%s</strong>. 此端口尚未提供Web服务</div></div></div></body></html>'
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()