Tunnel %s unavailable
Unable to initiate connection to %s. This port is not yet available for web server.
'
348 | html = body % (js['Payload']['Url'], localhost + ':' + str(localport))
349 | header = "HTTP/1.0 502 Bad Gateway" + "\r\n"
350 | header += "Content-Type: text/html" + "\r\n"
351 | header += "Content-Length: %d" + "\r\n"
352 | header += "\r\n" + "%s"
353 | buf = header % (len(html.encode('utf-8')), html)
354 | sendbuf(sock, buf.encode('utf-8'))
355 |
356 | if len(recvbuf) == (8 + lenbyte):
357 | recvbuf = bytes()
358 | else:
359 | recvbuf = recvbuf[8 + lenbyte:]
360 |
361 | if type == 3 or (type == 2 and linkstate == 2):
362 | sendbuf(tosock, recvbuf)
363 | recvbuf = bytes()
364 |
365 | except socket.error:
366 | break
367 |
368 | if type == 1:
369 | mainsocket = False
370 | if type == 3:
371 | try:
372 | tosock.shutdown(socket.SHUT_WR)
373 | except socket.error:
374 | tosock.close()
375 |
376 | logger = logging.getLogger('%s:%d' % ('Close', sock.fileno()))
377 | logger.debug('Closing')
378 | sock.close()
379 |
380 | # 客户端程序初始化
381 | if __name__ == '__main__':
382 | logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s', datefmt='%Y/%m/%d %H:%M:%S')
383 | logger = logging.getLogger('%s' % 'client')
384 | logger.info('python-ngrok v1.56')
385 | while True:
386 | try:
387 | # 检测控制连接是否连接.
388 | if mainsocket == False:
389 | mainsocket = connectremote(host, port)
390 | if mainsocket == False:
391 | logger = logging.getLogger('%s' % 'client')
392 | logger.info('connect failed...!')
393 | time.sleep(10)
394 | continue
395 | thread = threading.Thread(target = HKClient, args = (mainsocket, 0, 1))
396 | thread.setDaemon(True)
397 | thread.start()
398 |
399 | # 发送心跳
400 | if pingtime + 20 < time.time() and pingtime != 0:
401 | sendpack(mainsocket, Ping())
402 | pingtime = time.time()
403 |
404 | time.sleep(1)
405 |
406 | except socket.error:
407 | pingtime = 0
408 | except KeyboardInterrupt:
409 | sys.exit()
410 |
--------------------------------------------------------------------------------
/python-ngrok_deepseek.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python3
2 | # -*- coding: UTF-8 -*-
3 | # 建议Python 3.7.0 以上运行
4 | # 项目地址: https://github.com/hauntek/python-ngrok
5 | # Version: 2.2.0
6 | import asyncio
7 | import socket
8 | import ssl
9 | import json
10 | import struct
11 | import sys
12 | import time
13 | import secrets
14 | import logging
15 | from typing import Optional, Union, Dict, List
16 | from dataclasses import dataclass, asdict, fields
17 |
18 | # 配置日志格式
19 | logging.basicConfig(
20 | level=logging.INFO,
21 | format='[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s',
22 | datefmt='%Y/%m/%d %H:%M:%S'
23 | )
24 | logger = logging.getLogger('NgrokClient')
25 |
26 | # 定义认证消息的数据结构
27 | @dataclass
28 | class Auth:
29 | Version: str = "2" # 协议版本
30 | MmVersion: str = "1.7" # 主版本号
31 | User: str = "" # 用户名
32 | Password: str = "" # 密码
33 | OS: str = "darwin" # 操作系统
34 | Arch: str = "amd64" # 系统架构
35 | ClientId: str = "" # 客户端ID
36 |
37 | @classmethod
38 | def get_class_name(cls):
39 | """返回类名"""
40 | return cls.__name__
41 |
42 | # 定义认证响应消息的数据结构
43 | @dataclass
44 | class AuthResp:
45 | Version: str = "2" # 协议版本
46 | MmVersion: str = "1.7" # 主版本号
47 | ClientId: str = "" # 客户端ID
48 | Error: str = "" # 错误信息
49 |
50 | @classmethod
51 | def get_class_name(cls):
52 | """返回类名"""
53 | return cls.__name__
54 |
55 | # 定义请求隧道消息的数据结构
56 | @dataclass
57 | class ReqTunnel:
58 | ReqId: str = "" # 请求ID
59 | Protocol: str = "" # 协议类型(如 http、tcp)
60 | Hostname: str = "" # 主机名
61 | Subdomain: str = "" # 子域名
62 | HttpAuth: str = "" # HTTP 认证信息
63 | RemotePort: int = 0 # 远程端口
64 |
65 | @classmethod
66 | def get_class_name(cls):
67 | """返回类名"""
68 | return cls.__name__
69 |
70 | # 定义新隧道消息的数据结构
71 | @dataclass
72 | class NewTunnel:
73 | ReqId: str = "" # 请求ID
74 | Url: str = "" # 隧道URL
75 | Protocol: str = "" # 协议类型
76 | Error: str = "" # 错误信息
77 |
78 | @classmethod
79 | def get_class_name(cls):
80 | """返回类名"""
81 | return cls.__name__
82 |
83 | # 定义请求代理消息的数据结构
84 | @dataclass
85 | class ReqProxy:
86 | pass
87 |
88 | @classmethod
89 | def get_class_name(cls):
90 | """返回类名"""
91 | return cls.__name__
92 |
93 | # 定义注册代理消息的数据结构
94 | @dataclass
95 | class RegProxy:
96 | ClientId: str = "" # 客户端ID
97 |
98 | @classmethod
99 | def get_class_name(cls):
100 | """返回类名"""
101 | return cls.__name__
102 |
103 | # 定义启动代理消息的数据结构
104 | @dataclass
105 | class StartProxy:
106 | Url: str = "" # 代理URL
107 | ClientAddr: str = "" # 客户端地址
108 |
109 | @classmethod
110 | def get_class_name(cls):
111 | """返回类名"""
112 | return cls.__name__
113 |
114 | # 定义心跳消息的数据结构
115 | @dataclass
116 | class Ping:
117 | pass
118 |
119 | @classmethod
120 | def get_class_name(cls):
121 | """返回类名"""
122 | return cls.__name__
123 |
124 | # 定义心跳响应消息的数据结构
125 | @dataclass
126 | class Pong:
127 | pass
128 |
129 | @classmethod
130 | def get_class_name(cls):
131 | """返回类名"""
132 | return cls.__name__
133 |
134 | # 定义消息类型的联合类型
135 | MessageType = Union[Auth, AuthResp, ReqTunnel, NewTunnel, ReqProxy, RegProxy, StartProxy, Ping, Pong]
136 |
137 | # Ngrok 客户端配置类
138 | class NgrokConfig:
139 | def __init__(self):
140 | """初始化默认配置"""
141 | self.server_host = 'tunnel.qydev.com' # 服务器主机名
142 | self.server_port = 4443 # 服务器端口
143 | self.bufsize = 1024 # 缓冲区大小
144 | self.authtoken = '' # 认证令牌
145 | self.tunnels: List[Dict] = [] # 隧道配置列表
146 |
147 | # 添加默认隧道配置
148 | self.tunnels.append({
149 | 'protocol': 'http',
150 | 'hostname': 'www.xxx.com',
151 | 'subdomain': '',
152 | 'httpauth': '',
153 | 'rport': 0,
154 | 'lhost': '127.0.0.1',
155 | 'lport': 80
156 | })
157 | self.tunnels.append({
158 | 'protocol': 'http',
159 | 'hostname': '',
160 | 'subdomain': 'xxx',
161 | 'httpauth': '',
162 | 'rport': 0,
163 | 'lhost': '127.0.0.1',
164 | 'lport': 80
165 | })
166 | self.tunnels.append({
167 | 'protocol': 'tcp',
168 | 'hostname': '',
169 | 'subdomain': '',
170 | 'httpauth': '',
171 | 'rport': 55499,
172 | 'lhost': '127.0.0.1',
173 | 'lport': 22
174 | })
175 | self.tunnels.append({
176 | 'protocol': 'udp',
177 | 'hostname': '',
178 | 'subdomain': '',
179 | 'httpauth': '',
180 | 'rport': 55499,
181 | 'lhost': '127.0.0.1',
182 | 'lport': 53
183 | })
184 |
185 | @classmethod
186 | def from_file(cls, filename: str) -> 'NgrokConfig':
187 | """从配置文件加载配置"""
188 | config = cls()
189 | try:
190 | with open(filename, 'r') as f:
191 | data = json.load(f)
192 | config.server_host = data["server"]["host"]
193 | config.server_port = int(data["server"]["port"])
194 | config.bufsize = int(data["server"].get("bufsize", 1024))
195 | config.authtoken = data["server"].get("authtoken", "")
196 | config.tunnels = [
197 | {
198 | 'protocol': t["protocol"],
199 | 'hostname': t.get("hostname", ""),
200 | 'subdomain': t.get("subdomain", ""),
201 | 'httpauth': t.get("httpauth", ""),
202 | 'rport': int(t.get("rport", 0)),
203 | 'lhost': t["lhost"],
204 | 'lport': int(t["lport"])
205 | }
206 | for t in data["client"]
207 | ]
208 | except Exception as e:
209 | logger.error(f"配置文件加载失败: {str(e)}")
210 | raise
211 | return config
212 |
213 | # 代理连接处理器
214 | class ProxyConnection:
215 | def __init__(self, client: 'NgrokClient'):
216 | """初始化代理连接"""
217 | self.client = client
218 | self.url = None
219 | self.proxy_reader: Optional[asyncio.StreamReader] = None
220 | self.proxy_writer: Optional[asyncio.StreamWriter] = None
221 | self.local_reader: Optional[asyncio.StreamReader] = None
222 | self.local_writer: Optional[asyncio.StreamWriter] = None
223 | self.udp_transport: Optional[asyncio.DatagramTransport] = None
224 | self.local_queue: Optional[asyncio.Queue] = None
225 | self.tasks = []
226 | self.running = True
227 |
228 | async def start(self):
229 | """启动代理连接全流程"""
230 | try:
231 | # 建立新的代理连接
232 | await self._connect_proxy_server()
233 |
234 | # 发送 RegProxy 注册消息
235 | try:
236 | regproxy_msg = RegProxy(ClientId=self.client.client_id)
237 | await self.client._send_packet(self.proxy_writer, regproxy_msg)
238 | except Exception as e:
239 | logger.debug(f"发送数据时发生错误: {str(e)}")
240 | return
241 |
242 | # 等待 StartProxy 消息
243 | try:
244 | msg = await self.client._recv_packet(self.proxy_reader)
245 | if not msg:
246 | return
247 | if not isinstance(msg, StartProxy):
248 | logger.debug("未收到 StartProxy 消息")
249 | return
250 |
251 | if not msg.Url:
252 | logger.debug("未收到有效 URL")
253 | return
254 | self.url = msg.Url
255 | except Exception as e:
256 | logger.debug(f"接收数据时发生错误: {str(e)}")
257 | return
258 |
259 | protocol = self.url.split(":")[0]
260 | if protocol == 'udp':
261 | # 连接到本地 UDP 服务
262 | await self._connect_local_service_udp()
263 | # 启动双向数据桥接
264 | await self._bridge_data_udp()
265 | return
266 |
267 | # 连接到本地 TCP 服务
268 | await self._connect_local_service_tcp()
269 | # 启动双向数据桥接
270 | await self._bridge_data_tcp()
271 |
272 | except Exception as e:
273 | logger.error(f"代理连接失败: {str(e)}")
274 | finally:
275 | await self._cleanup()
276 |
277 | async def _connect_proxy_server(self):
278 | """连接到代理服务器"""
279 | try:
280 | self.proxy_reader, self.proxy_writer = await asyncio.open_connection(
281 | host=self.client.config.server_host,
282 | port=self.client.config.server_port,
283 | ssl=self.client.ssl_ctx,
284 | server_hostname=self.client.config.server_host
285 | )
286 | logger.debug(f"已建立代理连接到 {self.client.config.server_host}:{self.client.config.server_port}")
287 | except Exception as e:
288 | logger.error(f"代理服务器连接失败: {str(e)}")
289 | raise
290 |
291 | async def _connect_local_service_udp(self):
292 | """连接到本地 UDP 服务"""
293 | class LocalProtocol(asyncio.DatagramProtocol):
294 | def __init__(self, proxy_conn: ProxyConnection):
295 | self.proxy_conn = proxy_conn
296 | self.local_queue = asyncio.Queue()
297 |
298 | def datagram_received(self, data: bytes, addr: tuple[str, int]):
299 | """接收 UDP 数据"""
300 | self.local_queue.put_nowait(data)
301 |
302 | def error_received(self, exc: OSError):
303 | """处理 UDP 错误"""
304 | logger.error(f"UDP 错误: {exc}")
305 |
306 | local_host, local_port = self.client.tunnel_map[self.url]
307 | try:
308 | loop = asyncio.get_running_loop()
309 | transport, protocol = await loop.create_datagram_endpoint(
310 | lambda: LocalProtocol(self),
311 | remote_addr=(local_host, local_port)
312 | )
313 | self.udp_transport = transport
314 | self.local_queue = protocol.local_queue
315 | logger.info(f"已连接到本地 UDP 服务 {local_host}:{local_port}")
316 | except Exception as e:
317 | logger.error(f"本地 UDP 服务连接失败: {str(e)}")
318 | raise
319 |
320 | async def _connect_local_service_tcp(self):
321 | """连接到本地 TCP 服务"""
322 | local_host, local_port = self.client.tunnel_map[self.url]
323 | try:
324 | self.local_reader, self.local_writer = await asyncio.open_connection(
325 | host=local_host,
326 | port=local_port
327 | )
328 | logger.info(f"已连接到本地 TCP 服务 {local_host}:{local_port}")
329 | except Exception as e:
330 | logger.error(f"本地 TCP 服务连接失败: {str(e)}")
331 | raise
332 |
333 | async def _bridge_data_udp(self):
334 | """双向数据转发(UDP)"""
335 | async def tcp_to_udp(src: asyncio.StreamReader, label: str):
336 | """从 TCP 读取数据并转发到 UDP"""
337 | try:
338 | buffer = b''
339 | while self.running:
340 | data = await src.read(self.client.config.bufsize)
341 | if not data:
342 | logger.debug(f"{label} 连接正常关闭")
343 | break
344 | buffer += data
345 | while len(buffer) >= 8:
346 | pkt_len, _ = struct.unpack('
本地 UDP"))
379 | udp_task = asyncio.create_task(udp_to_tcp(self.local_queue, "服务端 TCP <- 本地 UDP"))
380 | self.tasks.extend([tcp_task, udp_task])
381 |
382 | done, pending = await asyncio.wait({udp_task, tcp_task}, return_when=asyncio.FIRST_COMPLETED)
383 |
384 | for task in pending:
385 | task.cancel()
386 | await asyncio.gather(*pending, return_exceptions=True)
387 |
388 | async def _bridge_data_tcp(self):
389 | """双向数据转发(TCP)"""
390 | async def forward(src: asyncio.StreamReader, dst: asyncio.StreamWriter, label: str):
391 | """从源读取数据并转发到目标"""
392 | try:
393 | while self.running:
394 | data = await src.read(self.client.config.bufsize)
395 | if not data:
396 | logger.debug(f"{label} 连接正常关闭")
397 | break
398 | dst.write(data)
399 | await dst.drain()
400 | logger.debug(f"{label} 转发 {len(data)} bytes")
401 | except asyncio.CancelledError:
402 | pass
403 | except Exception as e:
404 | if self.running:
405 | logger.error(f"{label} 转发错误: {str(e)}")
406 |
407 | task1 = asyncio.create_task(
408 | forward(self.proxy_reader, self.local_writer, "服务端 TCP -> 本地 TCP")
409 | )
410 | task2 = asyncio.create_task(
411 | forward(self.local_reader, self.proxy_writer, "服务端 TCP <- 本地 TCP")
412 | )
413 | self.tasks.extend([task1, task2])
414 |
415 | done, pending = await asyncio.wait({task1, task2}, return_when=asyncio.FIRST_COMPLETED)
416 |
417 | for task in pending:
418 | task.cancel()
419 | await asyncio.gather(*pending, return_exceptions=True)
420 |
421 | async def _cleanup(self):
422 | """资源清理"""
423 | self.running = False
424 |
425 | # 取消本连接创建的所有任务
426 | for task in self.tasks:
427 | task.cancel()
428 | await asyncio.gather(*self.tasks, return_exceptions=True)
429 |
430 | writers = [self.proxy_writer, self.local_writer]
431 | for writer in writers:
432 | if writer and not writer.is_closing():
433 | try:
434 | writer.close()
435 | await writer.wait_closed()
436 | except Exception as e:
437 | logger.debug(f"关闭 writer 时发生错误: {str(e)}")
438 |
439 | if self.udp_transport:
440 | try:
441 | self.udp_transport.close()
442 | except Exception as e:
443 | logger.debug(f"关闭 UDP 传输时发生错误: {str(e)}")
444 |
445 | if self.local_queue is not None:
446 | try:
447 | self.local_queue.put_nowait(None)
448 | except asyncio.QueueFull:
449 | await self.local_queue.put(None)
450 |
451 | # 从客户端移除本连接
452 | async with self.client.lock:
453 | if self in self.client.proxy_connections:
454 | self.client.proxy_connections.remove(self)
455 |
456 | # Ngrok 客户端类
457 | class NgrokClient:
458 | def __init__(self, config: NgrokConfig):
459 | """初始化 Ngrok 客户端"""
460 | self.config = config
461 | self.client_id = '' # 客户端ID
462 | self.last_ping = 0.0 # 上次心跳时间
463 | self.current_retry_interval = 1 # 当前重试间隔
464 | self.max_retry_interval = 60 # 最大重试间隔
465 | self.main_loop_task = None # 主循环任务
466 | self.main_reader: Optional[asyncio.StreamReader] = None # 主连接读取器
467 | self.main_writer: Optional[asyncio.StreamWriter] = None # 主连接写入器
468 | self.ssl_ctx = self._create_ssl_context() # SSL 上下文
469 | self.req_map: dict[str, tuple[str, int]] = {} # 请求ID到本地地址的映射
470 | self.tunnel_map: dict[str, tuple[str, int]] = {} # 隧道URL到本地地址的映射
471 | self.proxy_connections = [] # 代理连接列表
472 | self.lock = asyncio.Lock() # 异步锁
473 | self.running = True # 运行状态
474 | self._validate_tunnels() # 验证隧道配置
475 |
476 | def _create_ssl_context(self) -> ssl.SSLContext:
477 | """创建 SSL 上下文"""
478 | ctx = ssl.create_default_context()
479 | ctx.check_hostname = False
480 | ctx.verify_mode = ssl.CERT_NONE
481 | return ctx
482 |
483 | def _validate_tunnels(self):
484 | """验证隧道配置有效性"""
485 | required_fields = ['protocol', 'lhost', 'lport']
486 | for t in self.config.tunnels:
487 | for field in required_fields:
488 | if field not in t:
489 | raise ValueError(f"隧道配置缺少必要字段: {field}")
490 |
491 | async def _connect_server(self):
492 | """连接到服务器"""
493 | try:
494 | self.main_reader, self.main_writer = await asyncio.open_connection(
495 | host=self.config.server_host,
496 | port=self.config.server_port,
497 | ssl=self.ssl_ctx,
498 | server_hostname=self.config.server_host
499 | )
500 | logger.info(f"成功连接到服务器 {self.config.server_host}:{self.config.server_port}")
501 | except ConnectionRefusedError:
502 | logger.error(f"服务器拒绝连接: {self.config.server_host}:{self.config.server_port}")
503 | raise
504 | except Exception as e:
505 | logger.error(f"服务器连接失败: {str(e)}")
506 | raise
507 |
508 | async def _handle_auth(self):
509 | """处理认证流程"""
510 | auth_msg = Auth(ClientId=self.client_id, User=self.config.authtoken)
511 | await self._send_packet(self.main_writer, auth_msg)
512 |
513 | def dict_to_message(self, msg: dict):
514 | """
515 | 将字典转换为消息类型
516 | """
517 | msg_type = msg.get("Type")
518 | payload = msg.get("Payload", {})
519 | msg_classes = {
520 | "Auth": Auth,
521 | "AuthResp": AuthResp,
522 | "ReqTunnel": ReqTunnel,
523 | "NewTunnel": NewTunnel,
524 | "ReqProxy": ReqProxy,
525 | "RegProxy": RegProxy,
526 | "StartProxy": StartProxy,
527 | "Ping": Ping,
528 | "Pong": Pong
529 | }
530 | if msg_type in msg_classes:
531 | cls = msg_classes[msg_type]
532 | payload = {k: payload[k] for k in payload if k in {f.name for f in fields(cls)}}
533 | return cls(**payload)
534 | else:
535 | raise ValueError(f"未知消息类型: {msg_type}")
536 |
537 | async def _recv_packet(self, reader: asyncio.StreamReader) -> Optional[MessageType]:
538 | """接收协议数据包"""
539 | header = await reader.read(8)
540 | if not header:
541 | logger.debug("连接已关闭")
542 | return
543 | msg_len, _ = struct.unpack(' 20:
657 | try:
658 | await self._send_packet(self.main_writer, Ping())
659 | self.last_ping = time.time()
660 | except Exception as e:
661 | logger.debug(f"发送心跳失败: {str(e)}")
662 | self.running = False
663 | await asyncio.sleep(1)
664 |
665 | async def _main_loop(self):
666 | """业务逻辑主循环"""
667 | self.running = True
668 | self.main_loop_task = asyncio.gather(
669 | self._recv_loop(),
670 | self._heartbeat_task()
671 | )
672 | try:
673 | try:
674 | await self._handle_auth()
675 | except Exception as e:
676 | logger.debug(f"发送数据时发生错误: {str(e)}")
677 | raise
678 | # 启动接收和心跳任务
679 | await self.main_loop_task
680 | except asyncio.CancelledError:
681 | logger.debug("主循环任务被取消")
682 | raise
683 |
684 | async def _connect_with_retry(self):
685 | """带指数退避的连接方法"""
686 | while True:
687 | try:
688 | await self._connect_server()
689 | self.current_retry_interval = 1
690 | return
691 | except Exception as e:
692 | logger.error(f"连接失败,{self.current_retry_interval}秒后重试...")
693 | try:
694 | # 等待重连间隔
695 | await asyncio.sleep(self.current_retry_interval)
696 | except asyncio.CancelledError:
697 | logger.debug("重连等待被中断")
698 | raise
699 | self.current_retry_interval = min(
700 | self.current_retry_interval * 2,
701 | self.max_retry_interval
702 | )
703 |
704 | async def start(self):
705 | """启动客户端主循环"""
706 | while True:
707 | try:
708 | await self._connect_with_retry()
709 | await self._main_loop()
710 | except Exception as e:
711 | logger.error(f"运行时异常: {str(e)}")
712 | finally:
713 | await self._cleanup_resources()
714 | await self._wait_for_reconnect()
715 |
716 | logger.info("客户端已停止")
717 |
718 | if __name__ == '__main__':
719 | try:
720 | # 从配置文件加载配置,或使用默认配置
721 | config = NgrokConfig.from_file(sys.argv[1]) if len(sys.argv) > 1 else NgrokConfig()
722 | client = NgrokClient(config)
723 | asyncio.run(client.start())
724 | except KeyboardInterrupt:
725 | logger.info("用户中断操作")
726 | except Exception as e:
727 | logger.error(f"客户端异常终止: {str(e)}")
728 |
--------------------------------------------------------------------------------
/python-ngrok_gevent.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: UTF-8 -*-
3 | # 建议Python 2.7.9 或 Python 3.4.2 以上运行
4 | # 项目地址: https://github.com/hauntek/python-ngrok
5 | # Version: v1.56 gevent
6 | from gevent import monkey; monkey.patch_all()
7 |
8 | import socket
9 | import ssl
10 | import json
11 | import struct
12 | import random
13 | import sys
14 | import time
15 | import logging
16 | # import threading
17 |
18 | import gevent
19 |
20 | host = 'tunnel.qydev.com' # Ngrok服务器地址
21 | port = 4443 # 端口
22 | bufsize = 1024 # 吞吐量
23 |
24 | dualstack = 'IPv4/IPv6' # 服务连接协议 [IPv4/IPv6=双栈]
25 | dualstack_or = 0 # 本地转发协议 [0=双栈, 1=IPv4, 2=IPv6]
26 |
27 | Tunnels = list() # 全局渠道赋值
28 | body = dict()
29 | body['protocol'] = 'http'
30 | body['hostname'] = 'www.xxx.com'
31 | body['subdomain'] = ''
32 | body['rport'] = 0
33 | body['lhost'] = '127.0.0.1'
34 | body['lport'] = 80
35 | Tunnels.append(body) # 加入渠道队列
36 |
37 | body = dict()
38 | body['protocol'] = 'http'
39 | body['hostname'] = ''
40 | body['subdomain'] = 'xxx'
41 | body['rport'] = 0
42 | body['lhost'] = '127.0.0.1'
43 | body['lport'] = 80
44 | Tunnels.append(body) # 加入渠道队列
45 |
46 | body = dict()
47 | body['protocol'] = 'tcp'
48 | body['hostname'] = ''
49 | body['subdomain'] = ''
50 | body['rport'] = 55499
51 | body['lhost'] = '127.0.0.1'
52 | body['lport'] = 22
53 | Tunnels.append(body) # 加入渠道队列
54 |
55 | reqIdaddr = dict()
56 | localaddr = dict()
57 |
58 | # 读取配置文件
59 | if len(sys.argv) >= 2:
60 | file_object = open(sys.argv[1])
61 | try:
62 | all_the_text = file_object.read()
63 | config_object = json.loads(all_the_text)
64 | host = config_object["server"]["host"] # Ngrok服务器地址
65 | port = int(config_object["server"]["port"]) # 端口
66 | bufsize = int(config_object["server"]["bufsize"]) # 吞吐量
67 | Tunnels = list() # 重置渠道赋值
68 | for Tunnel in config_object["client"]:
69 | body = dict()
70 | body['protocol'] = Tunnel["protocol"]
71 | body['hostname'] = Tunnel["hostname"]
72 | body['subdomain'] = Tunnel["subdomain"]
73 | body['rport'] = int(Tunnel["rport"])
74 | body['lhost'] = Tunnel["lhost"]
75 | body['lport'] = int(Tunnel["lport"])
76 | Tunnels.append(body) # 加入渠道队列
77 | del all_the_text
78 | del config_object
79 | except Exception:
80 | # logger = logging.getLogger('%s' % 'config')
81 | # logger.error('The configuration file read failed')
82 | # exit(1)
83 | pass
84 | finally:
85 | file_object.close()
86 |
87 | mainsocket = 0
88 |
89 | ClientId = ''
90 | pingtime = 0
91 |
92 | def connectremote(host, port):
93 | ipv4_addr = list()
94 | ipv6_addr = list()
95 |
96 | for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, socket.SOCK_STREAM):
97 | af, socktype, proto, canonname, sa = res
98 |
99 | if dualstack == 'IPv4' or dualstack == 'IPv4/IPv6':
100 | if af == socket.AF_INET: ipv4_addr.append(res)
101 | if dualstack == 'IPv6' or dualstack == 'IPv4/IPv6':
102 | if af == socket.AF_INET6: ipv6_addr.append(res)
103 |
104 | if dualstack == 'IPv6' or dualstack == 'IPv4/IPv6':
105 | if len(ipv6_addr) > 0:
106 | for res in ipv6_addr:
107 | af, socktype, proto, canonname, sa = res
108 | try:
109 | client = socket.socket(af, socktype, proto)
110 | ssl_client = ssl.wrap_socket(client, ssl_version=ssl.PROTOCOL_SSLv23)
111 | except socket.error:
112 | continue
113 | try:
114 | ssl_client.connect(sa)
115 | ssl_client.setblocking(1)
116 | logger = logging.getLogger('%s:%d' % ('Conn', ssl_client.fileno()))
117 | logger.debug('New connection to: %s:%d' % (host, port))
118 |
119 | return ssl_client
120 | except socket.error:
121 | continue
122 |
123 | if dualstack == 'IPv4' or dualstack == 'IPv4/IPv6':
124 | if len(ipv4_addr) > 0:
125 | for res in ipv4_addr:
126 | af, socktype, proto, canonname, sa = res
127 | try:
128 | client = socket.socket(af, socktype, proto)
129 | ssl_client = ssl.wrap_socket(client, ssl_version=ssl.PROTOCOL_SSLv23)
130 | except socket.error:
131 | continue
132 | try:
133 | ssl_client.connect(sa)
134 | ssl_client.setblocking(1)
135 | logger = logging.getLogger('%s:%d' % ('Conn', ssl_client.fileno()))
136 | logger.debug('New connection to: %s:%d' % (host, port))
137 |
138 | return ssl_client
139 | except socket.error:
140 | continue
141 |
142 | return False
143 |
144 | def connectlocal(localhost, localport):
145 | ipv4_addr = list()
146 | ipv6_addr = list()
147 |
148 | for res in socket.getaddrinfo(localhost, localport, socket.AF_UNSPEC, socket.SOCK_STREAM):
149 | af, socktype, proto, canonname, sa = res
150 |
151 | if dualstack_or == 1 or dualstack_or == 0:
152 | if af == socket.AF_INET: ipv4_addr.append(res)
153 | if dualstack_or == 2 or dualstack_or == 0:
154 | if af == socket.AF_INET6: ipv6_addr.append(res)
155 |
156 | if dualstack_or == 2 or dualstack_or == 0:
157 | if len(ipv6_addr) > 0:
158 | for res in ipv6_addr:
159 | af, socktype, proto, canonname, sa = res
160 | try:
161 | client = socket.socket(af, socktype, proto)
162 | except socket.error:
163 | continue
164 | try:
165 | client.connect(sa)
166 | client.setblocking(1)
167 | logger = logging.getLogger('%s:%d' % ('Conn', client.fileno()))
168 | logger.debug('New connection to: %s:%d' % (host, port))
169 |
170 | return client
171 | except socket.error:
172 | continue
173 |
174 | if dualstack_or == 1 or dualstack_or == 0:
175 | if len(ipv4_addr) > 0:
176 | for res in ipv4_addr:
177 | af, socktype, proto, canonname, sa = res
178 | try:
179 | client = socket.socket(af, socktype, proto)
180 | except socket.error:
181 | continue
182 | try:
183 | client.connect(sa)
184 | client.setblocking(1)
185 | logger = logging.getLogger('%s:%d' % ('Conn', client.fileno()))
186 | logger.debug('New connection to: %s:%d' % (host, port))
187 |
188 | return client
189 | except socket.error:
190 | continue
191 |
192 | return False
193 |
194 | def NgrokAuth():
195 | Payload = dict()
196 | Payload['ClientId'] = ''
197 | Payload['OS'] = 'darwin'
198 | Payload['Arch'] = 'amd64'
199 | Payload['Version'] = '2'
200 | Payload['MmVersion'] = '1.7'
201 | Payload['User'] = 'user'
202 | Payload['Password'] = ''
203 | body = dict()
204 | body['Type'] = 'Auth'
205 | body['Payload'] = Payload
206 | buffer = json.dumps(body)
207 | return(buffer)
208 |
209 | def ReqTunnel(ReqId, Protocol, Hostname, Subdomain, RemotePort):
210 | Payload = dict()
211 | Payload['ReqId'] = ReqId
212 | Payload['Protocol'] = Protocol
213 | Payload['Hostname'] = Hostname
214 | Payload['Subdomain'] = Subdomain
215 | Payload['HttpAuth'] = ''
216 | Payload['RemotePort'] = RemotePort
217 | body = dict()
218 | body['Type'] = 'ReqTunnel'
219 | body['Payload'] = Payload
220 | buffer = json.dumps(body)
221 | return(buffer)
222 |
223 | def RegProxy(ClientId):
224 | Payload = dict()
225 | Payload['ClientId'] = ClientId
226 | body = dict()
227 | body['Type'] = 'RegProxy'
228 | body['Payload'] = Payload
229 | buffer = json.dumps(body)
230 | return(buffer)
231 |
232 | def Ping():
233 | Payload = dict()
234 | body = dict()
235 | body['Type'] = 'Ping'
236 | body['Payload'] = Payload
237 | buffer = json.dumps(body)
238 | return(buffer)
239 |
240 | def lentobyte(len):
241 | return struct.pack(' 0:
300 | if not recvbuf:
301 | recvbuf = recvbut
302 | else:
303 | recvbuf += recvbut
304 |
305 | if type == 1 or (type == 2 and linkstate == 1):
306 | lenbyte = tolen(recvbuf[0:8])
307 | if len(recvbuf) >= (8 + lenbyte):
308 | buf = recvbuf[8:lenbyte + 8].decode('utf-8')
309 | logger = logging.getLogger('%s:%d' % ('Recv', sock.fileno()))
310 | logger.debug('Reading message with length: %d' % len(buf))
311 | logger.debug('Read message: %s' % buf)
312 | js = json.loads(buf)
313 | if type == 1:
314 | if js['Type'] == 'ReqProxy':
315 | newsock = connectremote(host, port)
316 | if newsock:
317 | gevent.spawn(HKClient, newsock, 0, 2)
318 | if js['Type'] == 'AuthResp':
319 | ClientId = js['Payload']['ClientId']
320 | logger = logging.getLogger('%s' % 'client')
321 | logger.info('Authenticated with server, client id: %s' % ClientId)
322 | sendpack(sock, Ping())
323 | pingtime = time.time()
324 | for info in Tunnels:
325 | reqid = getRandChar(8)
326 | sendpack(sock, ReqTunnel(reqid, info['protocol'], info['hostname'], info['subdomain'], info['rport']))
327 | reqIdaddr[reqid] = (info['lhost'], info['lport'])
328 | if js['Type'] == 'NewTunnel':
329 | if js['Payload']['Error'] != '':
330 | logger = logging.getLogger('%s' % 'client')
331 | logger.error('Server failed to allocate tunnel: %s' % js['Payload']['Error'])
332 | time.sleep(30)
333 | else:
334 | logger = logging.getLogger('%s' % 'client')
335 | logger.info('Tunnel established at %s' % js['Payload']['Url'])
336 | localaddr[js['Payload']['Url']] = reqIdaddr[js['Payload']['ReqId']]
337 | if type == 2:
338 | if js['Type'] == 'StartProxy':
339 | localhost, localport = localaddr[js['Payload']['Url']]
340 |
341 | newsock = connectlocal(localhost, localport)
342 | if newsock:
343 | gevent.spawn(HKClient, newsock, 0, 3, sock)
344 | tosock = newsock
345 | linkstate = 2
346 | else:
347 | body = 'Tunnel %s unavailable
Unable to initiate connection to %s. This port is not yet available for web server.
'
348 | html = body % (js['Payload']['Url'], localhost + ':' + str(localport))
349 | header = "HTTP/1.0 502 Bad Gateway" + "\r\n"
350 | header += "Content-Type: text/html" + "\r\n"
351 | header += "Content-Length: %d" + "\r\n"
352 | header += "\r\n" + "%s"
353 | buf = header % (len(html.encode('utf-8')), html)
354 | sendbuf(sock, buf.encode('utf-8'))
355 |
356 | if len(recvbuf) == (8 + lenbyte):
357 | recvbuf = bytes()
358 | else:
359 | recvbuf = recvbuf[8 + lenbyte:]
360 |
361 | if type == 3 or (type == 2 and linkstate == 2):
362 | sendbuf(tosock, recvbuf)
363 | recvbuf = bytes()
364 |
365 | except socket.error:
366 | break
367 |
368 | if type == 1:
369 | mainsocket = False
370 | if type == 3:
371 | try:
372 | tosock.shutdown(socket.SHUT_WR)
373 | except socket.error:
374 | tosock.close()
375 |
376 | logger = logging.getLogger('%s:%d' % ('Close', sock.fileno()))
377 | logger.debug('Closing')
378 | sock.close()
379 |
380 | # 客户端程序初始化
381 | if __name__ == '__main__':
382 | logging.basicConfig(level=logging.DEBUG, format='[%(asctime)s] [%(levelname)s] [%(name)s] %(message)s', datefmt='%Y/%m/%d %H:%M:%S')
383 | logger = logging.getLogger('%s' % 'client')
384 | logger.info('python-ngrok v1.56')
385 | while True:
386 | try:
387 | # 检测控制连接是否连接.
388 | if mainsocket == False:
389 | mainsocket = connectremote(host, port)
390 | if mainsocket == False:
391 | logger = logging.getLogger('%s' % 'client')
392 | logger.info('connect failed...!')
393 | time.sleep(10)
394 | continue
395 | gevent.spawn(HKClient, mainsocket, 0, 1)
396 |
397 | # 发送心跳
398 | if pingtime + 20 < time.time() and pingtime != 0:
399 | sendpack(mainsocket, Ping())
400 | pingtime = time.time()
401 |
402 | time.sleep(1)
403 |
404 | except socket.error:
405 | pingtime = 0
406 | except KeyboardInterrupt:
407 | sys.exit()
408 |
--------------------------------------------------------------------------------