From 9cee40db866da58da882b851d7c1b12f15faa36a Mon Sep 17 00:00:00 2001 From: GVSADS Date: Thu, 29 Jan 2026 22:53:58 +0800 Subject: [PATCH] Initial commit of PyFrp project --- LICENSE | 21 ++ __init__.py | 2 + __pycache__/__init__.cpython-312.pyc | Bin 0 -> 250 bytes __pycache__/client.cpython-312.pyc | Bin 0 -> 17499 bytes __pycache__/server.cpython-312.pyc | Bin 0 -> 22632 bytes backup/__init__.py | 7 + backup/client.py | 208 ++++++++++++++ backup/readme.md | 181 +++++++++++++ backup/server.py | 324 ++++++++++++++++++++++ client.py | 295 ++++++++++++++++++++ readme.md | 388 +++++++++++++++++++++++++++ server.py | 343 +++++++++++++++++++++++ 12 files changed, 1769 insertions(+) create mode 100644 LICENSE create mode 100644 __init__.py create mode 100644 __pycache__/__init__.cpython-312.pyc create mode 100644 __pycache__/client.cpython-312.pyc create mode 100644 __pycache__/server.cpython-312.pyc create mode 100644 backup/__init__.py create mode 100644 backup/client.py create mode 100644 backup/readme.md create mode 100644 backup/server.py create mode 100644 client.py create mode 100644 readme.md create mode 100644 server.py diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..cda77d5 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Galaxy Vastar Software Studio + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..1bd58bc --- /dev/null +++ b/__init__.py @@ -0,0 +1,2 @@ +from .client import PortForwardClient +from .server import PortForwardServer diff --git a/__pycache__/__init__.cpython-312.pyc b/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..aa5d0307b685413bea8f80089038d576dfdc1fdf GIT binary patch literal 250 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 O{=5TX%h-G({{I3HHMxxd literal 0 HcmV?d00001 diff --git a/__pycache__/server.cpython-312.pyc b/__pycache__/server.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..caaad1e26d1d5d04fdf78388d6f488535a53bd63 GIT binary patch literal 22632 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 {Mapping.ForwardHost}:{Mapping.ForwardPort} ({Mapping.Mode})") + Mapping.IsActive = True + return True + else: + self.Log(f"Failed to register mapping: {Response.get('message', 'Unknown error')}") + return False + except Exception as E: + self.HandleError(f"Error registering mapping: {str(E)}") + return False + def RemoveMapping(self, TargetPort): + with self.MappingsLock: + for I, M in enumerate(self.Mappings): + if M.TargetPort == TargetPort: + if self.UnregisterMapping(M): + del self.Mappings[I] + return True + return False + self.Log(f"No mapping found for target port {TargetPort}") + return False + def UnregisterMapping(self, Mapping): + try: + Command = { + 'type': 'unregister', + 'timestamp': str(time.time()), + 'target_port': Mapping.TargetPort + } + EncryptedData = self.Encryptor.Encrypt(json.dumps(Command)) + self.Socket.sendall((EncryptedData + '\n').encode('utf-8')) + ResponseData = self.Socket.recv(4096).decode('utf-8').strip() + Response = json.loads(self.Encryptor.Decrypt(ResponseData)) + if Response.get('status') == 'success': + self.Log(f"Successfully unregistered mapping: {Mapping.TargetPort}") + Mapping.IsActive = False + return True + else: + self.Log(f"Failed to unregister mapping: {Response.get('message', 'Unknown error')}") + return False + except Exception as E: + self.HandleError(f"Error unregistering mapping: {str(E)}") + return False + def Start(self): + if not self.Connect(): + return + self.IsRunning = True + self.Log("Client started") + try: + # 连接成功后注册所有映射 + with self.MappingsLock: + for Mapping in self.Mappings: + self.RegisterMapping(Mapping) + while self.IsRunning: + time.sleep(1) + except KeyboardInterrupt: + self.Log("Client interrupted") + except Exception as E: + self.HandleError(f"Client error: {str(E)}") + finally: + self.Stop() + def Stop(self): + self.IsRunning = False + with self.MappingsLock: + for Mapping in self.Mappings: + if Mapping.IsActive: + self.UnregisterMapping(Mapping) + self.Mappings.clear() + if self.Socket: + try: + self.Socket.close() + except: + pass + self.Log("Client stopped") + + +if __name__ == "__main__": + Config = { + "server_host": "127.0.0.1", + "server_port": 5000, + "key": "07A36AEF1907843", + "mappings": [ + { + "forward_host": "127.0.0.1", + "forward_port": 5902, + "target_port": 5500, + "mode": "tcp" + } + ] + } + if len(sys.argv) > 1: + try: + with open(sys.argv[1], 'r') as F: + Config.update(json.load(F)) + except Exception as E: + print(f"Error loading config file: {str(E)}") + os._exit(0) + Client = PortForwardClient( + ServerHost=Config["server_host"], + ServerPort=int(Config["server_port"]), + Key=Config["key"] + ) + for Mapping in Config["mappings"]: + Client.AddMapping( + ForwardHost=Mapping["forward_host"], + ForwardPort=int(Mapping["forward_port"]), + TargetPort=int(Mapping["target_port"]), + Mode=Mapping["mode"] + ) + try: + Client.Start() + except KeyboardInterrupt: + Client.Stop() \ No newline at end of file diff --git a/backup/readme.md b/backup/readme.md new file mode 100644 index 0000000..2d4ac85 --- /dev/null +++ b/backup/readme.md @@ -0,0 +1,181 @@ +

+ +

PyFrp

+

Frp (Fast Reverse Proxy) implemented in Python

+

+ +![GitHub Followers](https://img.shields.io/badge/dynamic/json?color=green&label=GitHub%20Followers&query=%24.data.totalSubs&url=https%3A%2F%2Fapi.spencerwoo.com%2Fsubstats%2F%3Fsource%3Dgithub%26queryKey%3DGVSADS) +![Total Repos](https://img.shields.io/badge/dynamic/json?color=orange&label=Total%20Repos&query=%24.total_count&url=https%3A%2F%2Fapi.github.com%2Fsearch%2Frepositories%3Fq%3Duser%3AGVSADS) + +--- + +![Python](https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white) +![Operating System](https://img.shields.io/badge/Operating%20System-000000?style=for-the-badge&logo=linux&logoColor=white) +![Server](https://img.shields.io/badge/Server-000000?style=for-the-badge&logo=serverless&logoColor=white) +![Networking](https://img.shields.io/badge/Networking-000000?style=for-the-badge&logo=cisco&logoColor=white) + +--- +> **This project is only in its early stages, and the documentation is currently incomplete. Thank you for using this project. If possible, please give it a star ♥** + +# PyFrp + +## 🗡 Brief Introduction +The original Frp (https://github.com/Fatedier/frp/) is somewhat bloated in size, while PyFrp is a simplified Python-based implementation with a smaller footprint and fewer features. + +### Comparison with the Original Frp +| Feature| PyFrp | Frp| +|:---------------:|:-----:|:----:| +| Quick Setup| ✅| ✅| +| Size| ✅| ❌| +| SSL Support| ❌| ✅| +| Embeddable| ✅| ✅❌ | +| Documentation| ❌| ✅| + +Choose based on your needs: +- If you prioritize **quick setup, small size, simplicity, and embeddability** (e.g., integrating into your Python project), PyFrp is a great choice. +- For **production deployments**, Frp remains the better option for now. + +In the future, we aim to enhance PyFrp's functionality, add more configuration options, and potentially support additional protocols (e.g., HTTP/HTTPS). + +We’re just getting started and would greatly appreciate your support—**a ⭐ Star** would mean a lot to us! + +If needed, we could also develop a **C-based implementation** of Frp. + +> Note: **HTTPS is not yet supported**. We may add this later, and contributions are welcome! + +--- + +## 🚀 Quick Start + +The only third-party library you need is `pycryptodome`. Install it with: +```bash +pip install pycryptodome +``` +We use its encryption algorithms to secure your data. While any version of `pycryptodome` should work, we recommend **v3.22.0** if you encounter issues: +```bash +pip install pycryptodome==3.22.0 +``` +For a **non-encrypted version**, simply remove all encryption-related code. + +### Configuration +#### Server (`server_config.json`): +```python +{ + "internal_data_port": 5000, // PyFrp server data port + "allowed_port_range": "5001-5500", // Allowed port range + "max_ports_per_client": 5, // Max ports per client + "key": "07A36AEF1907843" // Encryption key +} +``` + +#### Client (`client_config.json`): +```python +{ + "server_host": "127.0.0.1", // PyFrp server address + "server_port": 5000, // PyFrp server port + "key": "07A36AEF1907843", // Encryption key + "mappings": [ // Port mappings + { + "forward_host": "127.0.0.1", // Local host + "forward_port": 5902, // Local port + "target_port": 5500, // Target port + "mode": "tcp" // Protocol (TCP/UDP) + } + // Add more mappings as needed + ] +} +``` + +Run with custom configs: +```bash +python server.py server_config.json +python client.py client_config.json +``` + +--- + +## 📞 Contact Us + - 📧 Email: wyt18222152539wyt@163.com + - 🌐 Website: [Galaxy Vastar Software Studio](https://www.gvsds.com) + - 📱 WeChat: GVSADS + +--- + +## 🗡 简单介绍 +原本的 Frp https://github.com/Fatedier/frp/ 体积略显臃肿,而 PyFrp 则是一个基于 Python 实现的简单版本,体积更小,功能也更简单。 + +我们 和 原版 Frp 相比 +| 功能 | PyFrp | Frp | +|:-:|:-:|:-:| +| 快速配置 | ✅ | ✅ | +| 体积 | ✅ | ❌ | +| SSL 功能 | ❌ | ✅ | +| 嵌入式 | ✅ | ✅❌ | +| 文档 | ❌ | ✅ | + +请您根据您的需要选择使用哪个, +- 如果您需要快速配置,体积小,功能简单,嵌入式,集成到您自己的 Python 项目中,那么 PyFrp 是一个不错的选择。 +- 如果您需要部署在生产环境中,暂时不要选择 PyFrp,Frp 是一个更好的选择。 + +日后,我们以 Frp 为目标,完善其功能,添加更多的配置项,同时也会考虑添加更多的协议支持,如 http、https 等。 + +我们其实也才起步,仍然需要您的支持,如果可以,还请您点一个 Star ⭐,这将是对我们最大的支持。 + +如果您有需要,我们可以设计一版 C 实现的 Frp。 + +> 注意,我们暂时还不支持 https 协议。稍后如果有时间,我们可能会考虑支持,如果您已经帮我们支持,随时欢迎您提交。 + +## 🚀 快速开始 + +你唯一需要下载的第三方库是 `pycryptodome`,使用以下命令安装: +```bash +pip install pycryptodome +``` +我们使用它的加密算法来保护你的数据。我们可知的是任意版本的 `pycryptodome` 都可以使用。但如果出现加密算法的报错,我们建议你使用最新版本的 `pycryptodome`,或者使用我们所使用的 3.22.0 版本,即: +```bash +pip install pycryptodome==3.22.0 +``` +如果你期望使用一个不带加密的版本,你可以将所有涉及加密的部分全部删掉。 + + + +然后,你可以修改源代码中的个人配置 + +server端配置修改: +```python +{ + "internal_data_port": 5000, // PyFrp 服务器端数据端口 + "allowed_port_range": "5001-5500", // 允许的端口范围 + "max_ports_per_client": 5, // 每个客户端最大端口数 + "key": "07A36AEF1907843" // 加密密钥 +} +``` + +client +```python +{ + "server_host": "127.0.0.1", // PyFrp 服务器端主机地址 + "server_port": 5000, // PyFrp 服务器端端口 + "key": "07A36AEF1907843", // 加密密钥 + "mappings": [ // 端口映射配置 + { + "forward_host": "127.0.0.1", // 本地主机地址 + "forward_port": 5902, // 本地端口 + "target_port": 5500, // 目标端口 + "mode": "tcp" // 传输模式 + }, + // 你可以在这里输入更多的端口映射配置 + ] +} +``` + +允许你直接修改代码中尾部的 Config 变量来实现自定义配置。或者指定配置文件位置。 +```bash +python server.py server_config.json +python client.py client_config.json +``` + +## 📞 联系我们 + - 📧 Email: wyt18222152539wyt@163.com + - 🌐 官网: [银河万通软件开发工作室](https://www.gvsds.com) + - 📱 微信: GVSADS diff --git a/backup/server.py b/backup/server.py new file mode 100644 index 0000000..194f478 --- /dev/null +++ b/backup/server.py @@ -0,0 +1,324 @@ +# -*- coding: utf-8 -*- +# Brief Introduction +# The original Frp (https://github.com/Fatedier/frp/) is somewhat bloated in size, +# while PyFrp is a simplified Python-based implementation with a smaller footprint and fewer features. + +import socket +import threading +import json +import traceback +import time +from Crypto.Cipher import AES +from Crypto.Util.Padding import pad, unpad +import base64 +import re +import sys +import os + +class AesEncryptor: + def __init__(self, Key): + self.Key = Key.ljust(32)[:32].encode('utf-8') + self.Mode = AES.MODE_CBC + def Encrypt(self, Data): + Iv = b'0123456789abcdef' + Cipher = AES.new(self.Key, self.Mode, Iv) + return base64.b64encode(Iv + Cipher.encrypt(pad(Data.encode('utf-8'), AES.block_size))).decode('utf-8') + def Decrypt(self, Data): + Data = base64.b64decode(Data) + Iv = Data[:16] + Cipher = AES.new(self.Key, self.Mode, Iv) + return unpad(Cipher.decrypt(Data[16:]), AES.block_size).decode('utf-8') + +class PortRange: + def __init__(self, RangeStr): + Match = re.match(r'^(\d+)-(\d+)$', RangeStr) + if not Match: + raise ValueError("Invalid port range format") + self.Start = int(Match.group(1)) + self.End = int(Match.group(2)) + if self.Start < 1 or self.End > 65535 or self.Start > self.End: + raise ValueError("Invalid port range values") + def IsInRange(self, Port): + return self.Start <= Port <= self.End + +class ClientHandler: + def __init__(self, Server, ClientSocket, Address): + self.Server = Server + self.ClientSocket = ClientSocket + self.Address = Address + self.IsRunning = True + self.Mappings = {} + self.Encryptor = AesEncryptor(Server.Key) + self.Log("New client connected") + def Log(self, Message): + Timestamp = time.strftime('%Y-%m-%d %H:%M:%S') + print(f"[{Timestamp}] [Client {self.Address}] {Message}") + def HandleError(self, Message): + self.Log(f"Error: {Message}") + self.Log(traceback.format_exc()) + def Close(self): + self.IsRunning = False + for Mapping in self.Mappings.values(): + Mapping.Close() + self.Mappings.clear() + try: + self.ClientSocket.close() + except: + pass + self.Log("Client disconnected") + def CreateTcpMapping(self, ForwardHost, ForwardPort, TargetPort): + try: + ServerSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + ServerSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + ServerSocket.bind(('0.0.0.0', TargetPort)) + ServerSocket.listen(5) + Mapping = TcpMapping(self, ServerSocket, ForwardHost, ForwardPort, TargetPort) + self.Mappings[TargetPort] = Mapping + threading.Thread(target=Mapping.Start, daemon=True).start() + return True + except Exception as E: + self.HandleError(f"Failed to create TCP mapping: {str(E)}") + return False + def RemoveMapping(self, TargetPort): + if TargetPort in self.Mappings: + self.Mappings[TargetPort].Close() + del self.Mappings[TargetPort] + return True + return False + def ProcessCommand(self, Command): + try: + CmdType = Command.get('type') + if CmdType == 'register': + if len(self.Mappings) >= self.Server.MaxPortsPerClient: + return {'status': 'error', 'message': f'Max {self.Server.MaxPortsPerClient} ports per client'} + TargetPort = Command.get('target_port') + if not self.Server.AllowedPorts.IsInRange(TargetPort): + return {'status': 'error', 'message': f'Target port {TargetPort} not in allowed range'} + if TargetPort in self.Server.GetAllUsedPorts(): + return {'status': 'error', 'message': f'Target port {TargetPort} already in use'} + Mode = Command.get('mode', 'tcp').lower() + ForwardHost = Command.get('forward_host', '127.0.0.1') + ForwardPort = Command.get('forward_port') + if not ForwardPort: + return {'status': 'error', 'message': 'Forward port is required'} + Success = False + if Mode == 'tcp': + Success = self.CreateTcpMapping(ForwardHost, ForwardPort, TargetPort) + else: + return {'status': 'error', 'message': f'Unsupported mode {Mode}'} + if Success: + return {'status': 'success', 'message': f'Mapping created: {TargetPort} -> {ForwardHost}:{ForwardPort} ({Mode})'} + else: + return {'status': 'error', 'message': 'Failed to create mapping'} + elif CmdType == 'unregister': + TargetPort = Command.get('target_port') + if self.RemoveMapping(TargetPort): + return {'status': 'success', 'message': f'Mapping removed: {TargetPort}'} + else: + return {'status': 'error', 'message': f'No mapping found for {TargetPort}'} + else: + return {'status': 'error', 'message': f'Unknown command type {CmdType}'} + except Exception as E: + self.HandleError(f"Error processing command: {str(E)}") + return {'status': 'error', 'message': str(E)} + def Run(self): + try: + self.ClientSocket.settimeout(30) + while self.IsRunning: + Data = b'' + while True: + try: + Chunk = self.ClientSocket.recv(1024) + if not Chunk: + self.Log("Connection closed by client") + self.Close() + return + Data += Chunk + if b'\n' in Data: + break + except socket.timeout: + continue + except Exception as E: + self.HandleError(f"Receive error: {str(E)}") + self.Close() + return + try: + EncryptedData = Data.decode('utf-8').strip() + DecryptedData = self.Encryptor.Decrypt(EncryptedData) + Command = json.loads(DecryptedData) + Timestamp = Command.get('timestamp') + if not Timestamp or abs(time.time() - float(Timestamp)) > 30: + Response = {'status': 'error', 'message': 'Invalid or expired timestamp'} + else: + Response = self.ProcessCommand(Command) + Response['timestamp'] = str(time.time()) + EncryptedResponse = self.Encryptor.Encrypt(json.dumps(Response)) + self.ClientSocket.sendall((EncryptedResponse + '\n').encode('utf-8')) + except Exception as E: + self.HandleError(f"Data processing error: {str(E)}") + Response = {'status': 'error', 'message': 'Invalid data format'} + EncryptedResponse = self.Encryptor.Encrypt(json.dumps(Response)) + self.ClientSocket.sendall((EncryptedResponse + '\n').encode('utf-8')) + except Exception as E: + self.HandleError(f"Client handler error: {str(E)}") + finally: + self.Close() + +class TcpMapping: + def __init__(self, ClientHandler, ServerSocket, ForwardHost, ForwardPort, TargetPort): + self.ClientHandler = ClientHandler + self.ServerSocket = ServerSocket + self.ForwardHost = ForwardHost + self.ForwardPort = ForwardPort + self.TargetPort = TargetPort + self.IsRunning = True + self.ClientHandler.Log(f"TCP mapping created: {TargetPort} -> {ForwardHost}:{ForwardPort}") + def Log(self, Message): + self.ClientHandler.Log(f"[TCP {self.TargetPort}] {Message}") + def HandleError(self, Message): + self.Log(f"Error: {Message}") + self.Log(traceback.format_exc()) + def Close(self): + self.IsRunning = False + try: + self.ServerSocket.close() + except: + pass + self.Log("Mapping closed") + def Relay(self, SrcSocket, DstSocket, Direction): + try: + while self.IsRunning: + Data = SrcSocket.recv(4096) + if not Data: + break + DstSocket.sendall(Data) + self.Log(f"Relayed {len(Data)} bytes {Direction}") + except Exception as E: + self.HandleError(f"Relay error {Direction}: {str(E)}") + finally: + try: + SrcSocket.close() + except: + pass + try: + DstSocket.close() + except: + pass + def HandleConnection(self, ClientSocket, Address): + self.Log(f"New connection from {Address}") + try: + ForwardSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + ForwardSocket.connect((self.ForwardHost, self.ForwardPort)) + self.Log(f"Connected to forward target {self.ForwardHost}:{self.ForwardPort}") + threading.Thread(target=self.Relay, args=(ClientSocket, ForwardSocket, f"to {self.ForwardHost}:{self.ForwardPort}"), daemon=True).start() + threading.Thread(target=self.Relay, args=(ForwardSocket, ClientSocket, f"from {self.ForwardHost}:{self.ForwardPort}"), daemon=True).start() + except Exception as E: + self.HandleError(f"Failed to connect to forward target: {str(E)}") + try: + ClientSocket.close() + except: + pass + def Start(self): + try: + while self.IsRunning: + self.ServerSocket.settimeout(1) + try: + ClientSocket, Address = self.ServerSocket.accept() + ClientSocket.settimeout(None) + threading.Thread(target=self.HandleConnection, args=(ClientSocket, Address), daemon=True).start() + except socket.timeout: + continue + except Exception as E: + if self.IsRunning: + self.HandleError(f"Accept error: {str(E)}") + break + except Exception as E: + self.HandleError(f"Mapping error: {str(E)}") + finally: + self.Close() + +class PortForwardServer: + def __init__(self, InternalPort=5000, AllowedPortsRange="5001-5500", MaxPortsPerClient=5, Key="07A36AEF1907843"): + self.InternalPort = InternalPort + self.AllowedPorts = PortRange(AllowedPortsRange) + self.MaxPortsPerClient = MaxPortsPerClient + self.Key = Key + self.ServerSocket = None + self.IsRunning = False + self.Clients = [] + self.ClientsLock = threading.Lock() + def Log(self, Message): + Timestamp = time.strftime('%Y-%m-%d %H:%M:%S') + print(f"[{Timestamp}] [Server] {Message}") + def HandleError(self, Message): + self.Log(f"Error: {Message}") + self.Log(traceback.format_exc()) + def GetAllUsedPorts(self): + UsedPorts = set() + with self.ClientsLock: + for Client in self.Clients: + UsedPorts.update(Client.Mappings.keys()) + return UsedPorts + def RemoveClient(self, Client): + with self.ClientsLock: + if Client in self.Clients: + self.Clients.remove(Client) + self.Log(f"Client removed. Total clients: {len(self.Clients)}") + def Start(self): + try: + self.ServerSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.ServerSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.ServerSocket.bind(('0.0.0.0', self.InternalPort)) + self.ServerSocket.listen(5) + self.IsRunning = True + self.Log(f"Server started on port {self.InternalPort}") + self.Log(f"Allowed ports: {self.AllowedPorts.Start}-{self.AllowedPorts.End}") + self.Log(f"Max ports per client: {self.MaxPortsPerClient}") + while self.IsRunning: + ClientSocket, Address = self.ServerSocket.accept() + self.Log(f"New connection from {Address}") + Client = ClientHandler(self, ClientSocket, Address) + with self.ClientsLock: + self.Clients.append(Client) + self.Log(f"Client added. Total clients: {len(self.Clients)}") + threading.Thread(target=lambda: [Client.Run(), self.RemoveClient(Client)], daemon=True).start() + except Exception as E: + self.HandleError(f"Server error: {str(E)}") + self.Stop() + def Stop(self): + self.IsRunning = True + if self.ServerSocket: + try: + self.ServerSocket.close() + except: + pass + with self.ClientsLock: + for Client in self.Clients: + Client.Close() + self.Clients.clear() + self.Log("Server stopped") + +if __name__ == "__main__": + Config = { + "internal_data_port": 5000, + "allowed_port_range": "5001-5500", + "max_ports_per_client": 5, + "key": "07A36AEF1907843" + } + if len(sys.argv) > 1: + try: + with open(sys.argv[1], 'r') as F: + Config.update(json.load(F)) + except Exception as E: + print(f"Error loading config file: {str(E)}") + os._exit(0) + Server = PortForwardServer( + InternalPort=int(Config["internal_data_port"]), + AllowedPortsRange=Config["allowed_port_range"], + MaxPortsPerClient=int(Config["max_ports_per_client"]), + Key=Config["key"] + ) + try: + Server.Start() + except KeyboardInterrupt: + Server.Stop() diff --git a/client.py b/client.py new file mode 100644 index 0000000..61f557b --- /dev/null +++ b/client.py @@ -0,0 +1,295 @@ +import socket +import threading +import json +import traceback +import sys +import time + +class PortForwardClient: + def __init__(self, ServerDomain="127.0.0.1", ServerPort=5000, Forwards=None, Key="07A36AEF1907843"): + self.ServerDomain = ServerDomain + self.ServerPort = ServerPort + self.Forwards = Forwards or [] + self.Key = Key + self.ServerSocket = None + self.Running = True + self.ForwardMap = {} + self.ConnectionMap = {} + self.Lock = threading.Lock() + self.Buffer = b'' + self.MessageSeparator = b'|||' + + def Start(self): + try: + self.ServerSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.ServerSocket.connect((self.ServerDomain, self.ServerPort)) + print(f"Connected to server {self.ServerDomain}:{self.ServerPort}") + self.Authenticate() + threading.Thread(target=self.ReceiveFromServer, daemon=True).start() + self.SetupForwards() + while self.Running: + time.sleep(5) + except Exception as e: + print(f"Client start error: {e}") + traceback.print_exc() + self.Stop() + + def Stop(self): + self.Running = False + with self.Lock: + for forwardId, forwardData in self.ForwardMap.items(): + try: + self.SendToServer({'type': 'close_forward', 'forward_id': forwardId}) + except: + pass + for connId, conn in forwardData['connections'].items(): + try: + conn.close() + except: + pass + self.ForwardMap.clear() + self.ConnectionMap.clear() + if self.ServerSocket: + try: + self.ServerSocket.close() + except: + pass + print("Client stopped") + + def Authenticate(self): + self.SendToServer({'type': 'auth', 'key': self.Key}) + response = self.ServerSocket.recv(4096) + if not response: + raise ConnectionError("Server closed connection during authentication") + self.Buffer += response + self.ProcessBuffer() + + def SetupForwards(self): + for forward in self.Forwards: + forwardDomain = forward.get('forward_domain', '127.0.0.1') + forwardPort = forward.get('forward_port') + targetPort = forward.get('target_port') + mode = forward.get('mode', 'tcp').upper() + if not all([forwardPort, targetPort]): + print("Invalid forward configuration, skipping") + continue + self.SendToServer({ + 'type': 'forward_request', + 'forward_domain': forwardDomain, + 'forward_port': forwardPort, + 'target_port': targetPort, + 'mode': mode + }) + + def ReceiveFromServer(self): + while self.Running and self.ServerSocket: + try: + self.ServerSocket.settimeout(1) + data = self.ServerSocket.recv(4096) + if not data: + print("Server disconnected") + self.Running = False + break + self.Buffer += data + self.ProcessBuffer() + except socket.timeout: + continue + except Exception as e: + print(f"Server communication error: {e}") + traceback.print_exc() + self.Running = False + break + + def ProcessBuffer(self): + while self.MessageSeparator in self.Buffer: + msgEnd = self.Buffer.index(self.MessageSeparator) + messageData = self.Buffer[:msgEnd] + self.Buffer = self.Buffer[msgEnd + len(self.MessageSeparator):] + try: + message = json.loads(messageData.decode('utf-8')) + self.ProcessServerMessage(message) + except json.JSONDecodeError: + print("Received invalid JSON from server") + except Exception as e: + print(f"Error processing server data: {e}") + traceback.print_exc() + + def ProcessServerMessage(self, message): + if message.get('type') == 'forward_response': + self.HandleForwardResponse(message) + elif message.get('type') == 'new_connection': + self.HandleNewConnection(message) + elif message.get('type') == 'data': + self.HandleData(message) + elif message.get('type') == 'close_connection': + self.HandleCloseConnection(message) + elif message.get('type') == 'error': + print(f"Server error: {message.get('message')}") + + def HandleForwardResponse(self, message): + if message.get('success'): + forwardId = message.get('forward_id') + targetPort = message.get('target_port') + forwardConfig = next((f for f in self.Forwards if f.get('target_port') == targetPort), None) + if forwardConfig and forwardId: + with self.Lock: + self.ForwardMap[forwardId] = { + 'config': forwardConfig, + 'connections': {} + } + print(f"Forward established: {forwardId}") + else: + print(f"Received forward response for unknown target port {targetPort}") + else: + print(f"Forward request failed: {message.get('message')}") + + def HandleNewConnection(self, message): + forwardId = message.get('forward_id') + connId = message.get('conn_id') + if not all([forwardId, connId]): + return + with self.Lock: + if forwardId not in self.ForwardMap: + print(f"Received connection for unknown forward {forwardId}") + return + forwardData = self.ForwardMap[forwardId] + config = forwardData['config'] + try: + if config.get('mode', 'tcp').upper() == 'TCP': + conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + conn.connect((config['forward_domain'], config['forward_port'])) + with self.Lock: + forwardData['connections'][connId] = conn + self.ConnectionMap[connId] = forwardId + threading.Thread(target=self.ForwardToServer, args=(forwardId, connId, conn), daemon=True).start() + print(f"Established connection {connId} for forward {forwardId}") + else: + print(f"Unsupported mode for forward {forwardId}") + except Exception as e: + print(f"Error establishing connection for {forwardId}: {e}") + traceback.print_exc() + self.SendToServer({ + 'type': 'close_connection', + 'forward_id': forwardId, + 'conn_id': connId + }) + + def ForwardToServer(self, forwardId, connId, conn): + try: + while self.Running: + conn.settimeout(1) + try: + with self.Lock: + if connId not in self.ConnectionMap or self.ConnectionMap[connId] != forwardId: + break + data = conn.recv(4096) + if not data: + break + self.SendToServer({ + 'type': 'data', + 'forward_id': forwardId, + 'conn_id': connId, + 'data': data.hex() + }) + except socket.timeout: + continue + except Exception as e: + print(f"Forward to server error: {e}") + traceback.print_exc() + break + finally: + try: + conn.close() + except: + pass + with self.Lock: + if forwardId in self.ForwardMap and connId in self.ForwardMap[forwardId]['connections']: + del self.ForwardMap[forwardId]['connections'][connId] + if connId in self.ConnectionMap: + del self.ConnectionMap[connId] + self.SendToServer({ + 'type': 'close_connection', + 'forward_id': forwardId, + 'conn_id': connId + }) + print(f"Closed connection {connId} for forward {forwardId}") + + def HandleData(self, message): + forwardId = message.get('forward_id') + connId = message.get('conn_id') + dataHex = message.get('data') + if not all([forwardId, connId, dataHex]): + return + try: + data = bytes.fromhex(dataHex) + with self.Lock: + if forwardId not in self.ForwardMap or connId not in self.ForwardMap[forwardId]['connections']: + print(f"Received data for unknown connection {connId}") + return + conn = self.ForwardMap[forwardId]['connections'][connId] + conn.sendall(data) + except Exception as e: + print(f"Data handling error: {e}") + traceback.print_exc() + self.SendToServer({ + 'type': 'close_connection', + 'forward_id': forwardId, + 'conn_id': connId + }) + + def HandleCloseConnection(self, message): + forwardId = message.get('forward_id') + connId = message.get('conn_id') + with self.Lock: + if forwardId in self.ForwardMap and connId in self.ForwardMap[forwardId]['connections']: + try: + self.ForwardMap[forwardId]['connections'][connId].close() + except: + pass + del self.ForwardMap[forwardId]['connections'][connId] + if connId in self.ConnectionMap: + del self.ConnectionMap[connId] + print(f"Connection {connId} for forward {forwardId} closed by server") + + def SendToServer(self, message): + if not self.ServerSocket or not self.Running: + return + try: + data = json.dumps(message).encode('utf-8') + self.MessageSeparator + self.ServerSocket.sendall(data) + except Exception as e: + print(f"Error sending to server: {e}") + traceback.print_exc() + self.Running = False + +def main(): + config = { + "ServerDomain": "127.0.0.1", + "ServerPort": 5000, + "Key": "07A36AEF1907843", + "Forwards": [ + { + "forward_domain": "127.0.0.1", + "forward_port": 36667, + "target_port": 5002, + "mode": "TCP" + } + ] + } + if len(sys.argv) > 1: + try: + with open(sys.argv[1], 'r') as f: + config.update(json.load(f)) + except Exception as e: + print(f"Error loading config file: {e}") + traceback.print_exc() + client = PortForwardClient( + ServerDomain=config["ServerDomain"], + ServerPort=int(config["ServerPort"]), + Forwards=config["Forwards"], + Key=config["Key"] + ) + client.Start() + +if __name__ == "__main__": + main() diff --git a/readme.md b/readme.md new file mode 100644 index 0000000..ba6d0c1 --- /dev/null +++ b/readme.md @@ -0,0 +1,388 @@ +

+ +

PyFrp

+

Frp (Fast Reverse Proxy) implemented in Python

+

+ +![GitHub Followers](https://img.shields.io/badge/dynamic/json?color=green&label=GitHub%20Followers&query=%24.data.totalSubs&url=https%3A%2F%2Fapi.spencerwoo.com%2Fsubstats%2F%3Fsource%3Dgithub%26queryKey%3DGVSADS) +![Total Repos](https://img.shields.io/badge/dynamic/json?color=orange&label=Total%20Repos&query=%24.total_count&url=https%3A%2F%2Fapi.github.com%2Fsearch%2Frepositories%3Fq%3Duser%3AGVSADS) + +--- + +![Python](https://img.shields.io/badge/Python-3776AB?style=for-the-badge&logo=python&logoColor=white) +![Operating System](https://img.shields.io/badge/Operating%20System-000000?style=for-the-badge&logo=linux&logoColor=white) +![Server](https://img.shields.io/badge/Server-000000?style=for-the-badge&logo=serverless&logoColor=white) +![Networking](https://img.shields.io/badge/Networking-000000?style=for-the-badge&logo=cisco&logoColor=white) + +--- +> **This project is only in its early stages, and the documentation is currently being improved. Thank you for using this project. If possible, please give it a star ♥** + +# PyFrp + +## 🗡 Brief Introduction +The original Frp (https://github.com/Fatedier/frp/) is somewhat bloated in size, while PyFrp is a simplified Python-based implementation with a smaller footprint and fewer features. + +### Comparison with the Original Frp +| Feature| PyFrp | Frp| +|:---------------:|:-----:|:----:| +| Quick Setup| ✅| ✅| +| Size| ✅| ❌| +| SSL Support| ❌| ✅| +| Embeddable| ✅| ✅❌ | +| Documentation| ✅| ✅| +| TCP Support| ✅| ✅| +| UDP Support| ❌| ✅| +| HTTP/HTTPS Support| ❌| ✅| + +Choose based on your needs: +- If you prioritize **quick setup, small size, simplicity, and embeddability** (e.g., integrating into your Python project), PyFrp is a great choice. +- For **production deployments**, Frp remains the better option for now. + +In the future, we aim to enhance PyFrp's functionality, add more configuration options, and potentially support additional protocols (e.g., HTTP/HTTPS). + +We’re just getting started and would greatly appreciate your support—**a ⭐ Star** would mean a lot to us! + +> Note: **HTTPS is not yet supported**. We may add this later, and contributions are welcome! + +--- + +## 🚀 Quick Start + +### Prerequisites + +The only third-party library you need is `pycryptodome`. Install it with: +```bash +pip install pycryptodome +``` +While any version of `pycryptodome` should work, we recommend **v3.22.0** if you encounter issues: +```bash +pip install pycryptodome==3.22.0 +``` + +### Configuration + +#### Server (`server_config.json`): +```json +{ + "InternalDataPort": 5000, // PyFrp server data port + "AllowedPortRange": "5001-5500", // Allowed port range + "MaxPortsPerClient": 5, // Max ports per client + "Key": "07A36AEF1907843" // Authentication key +} +``` + +#### Client (`client_config.json`): +```json +{ + "ServerDomain": "127.0.0.1", // PyFrp server address + "ServerPort": 5000, // PyFrp server port + "Key": "07A36AEF1907843", // Authentication key + "Forwards": [ // Port mappings + { + "forward_domain": "127.0.0.1", // Local host + "forward_port": 36667, // Local port + "target_port": 5002, // Target port + "mode": "TCP" // Protocol (TCP only) + } + // Add more mappings as needed + ] +} +``` + +### Running PyFrp + +#### Start the server: +```bash +python server.py server_config.json +``` + +#### Start the client: +```bash +python client.py client_config.json +``` + +You can also modify the default configuration directly in the source code. + +--- + +## 📖 Usage Example + +### Example: Exposing a Local HTTP Server + +1. **Start a local HTTP server**: + ```bash + python -m http.server 36667 + ``` + +2. **Configure PyFrp client** (`client_config.json`): + ```json + { + "ServerDomain": "127.0.0.1", + "ServerPort": 5000, + "Key": "07A36AEF1907843", + "Forwards": [ + { + "forward_domain": "127.0.0.1", + "forward_port": 36667, + "target_port": 5002, + "mode": "TCP" + } + ] + } + ``` + +3. **Start PyFrp server and client**: + ```bash + # In terminal 1 + python server.py + + # In terminal 2 + python client.py + ``` + +4. **Access the local server through the proxy**: + ```bash + curl http://localhost:5002 + ``` + + You should see the HTTP response from your local server, indicating that the port forwarding is working correctly. + +--- + +## ⚠️ Limitations + +- **Only TCP protocol is supported** (UDP, HTTP, HTTPS are not supported) +- **No SSL encryption** for data transmission +- **Simple authentication mechanism** (fixed key) +- **No automatic reconnection** after network interruption +- **Limited error handling** and logging +- **Performance limitations** due to JSON and hex encoding for data transfer + +--- + +## 🛠️ How It Works + +### Server-side (server.py) +1. Listens for client connections on the internal data port +2. Handles client authentication +3. Creates and manages port forwarding services +4. Forwards data between external connections and clients + +### Client-side (client.py) +1. Connects to the server and authenticates +2. Sends port forwarding configuration +3. Handles connections to local services +4. Forwards data between the server and local services + +--- + +## 🤝 Contributing + +Contributions are welcome! Here's how you can help: + +1. **Report bugs** by opening an issue +2. **Suggest features** by opening an issue +3. **Submit pull requests** with bug fixes or enhancements + +### Development Guidelines +- Follow the existing code style +- Add comments for complex code +- Test your changes thoroughly +- Update documentation as needed + +--- + +## 📞 Contact Us + - 📧 Email: wyt18222152539wyt@163.com + - 🌐 Website: [Galaxy Vastar Software Studio](https://www.gvsds.com) + - 📱 WeChat: GVSADS + +--- + +## 📄 License + +This project is licensed under the MIT License. See the LICENSE file for details. + +--- + +## 🗡 简单介绍 +原本的 Frp https://github.com/Fatedier/frp/ 体积略显臃肿,而 PyFrp 则是一个基于 Python 实现的简单版本,体积更小,功能也更简单。 + +我们 和 原版 Frp 相比 +| 功能 | PyFrp | Frp | +|:-:|:-:|:-:| +| 快速配置 | ✅ | ✅ | +| 体积 | ✅ | ❌ | +| SSL 功能 | ❌ | ✅ | +| 嵌入式 | ✅ | ✅❌ | +| 文档 | ✅ | ✅ | +| TCP 支持 | ✅ | ✅ | +| UDP 支持 | ❌ | ✅ | +| HTTP/HTTPS 支持 | ❌ | ✅ | + +请您根据您的需要选择使用哪个, +- 如果您需要快速配置,体积小,功能简单,嵌入式,集成到您自己的 Python 项目中,那么 PyFrp 是一个不错的选择。 +- 如果您需要部署在生产环境中,暂时不要选择 PyFrp,Frp 是一个更好的选择。 + +日后,我们以 Frp 为目标,完善其功能,添加更多的配置项,同时也会考虑添加更多的协议支持,如 http、https 等。 + +我们其实也才起步,仍然需要您的支持,如果可以,还请您点一个 Star ⭐,这将是对我们最大的支持。 + +> 注意,我们暂时还不支持 https 协议。稍后如果有时间,我们可能会考虑支持,如果您已经帮我们支持,随时欢迎您提交。 + +## 🚀 快速开始 + +### 先决条件 + +你唯一需要下载的第三方库是 `pycryptodome`,使用以下命令安装: +```bash +pip install pycryptodome +``` +我们推荐使用 **v3.22.0** 版本: +```bash +pip install pycryptodome==3.22.0 +``` + +### 配置 + +#### 服务器端配置 (`server_config.json`): +```json +{ + "InternalDataPort": 5000, // PyFrp 服务器端数据端口 + "AllowedPortRange": "5001-5500", // 允许的端口范围 + "MaxPortsPerClient": 5, // 每个客户端最大端口数 + "Key": "07A36AEF1907843" // 认证密钥 +} +``` + +#### 客户端配置 (`client_config.json`): +```json +{ + "ServerDomain": "127.0.0.1", // PyFrp 服务器端主机地址 + "ServerPort": 5000, // PyFrp 服务器端端口 + "Key": "07A36AEF1907843", // 认证密钥 + "Forwards": [ // 端口映射配置 + { + "forward_domain": "127.0.0.1", // 本地主机地址 + "forward_port": 36667, // 本地端口 + "target_port": 5002, // 目标端口 + "mode": "TCP" // 传输模式(仅支持TCP) + } + // 你可以在这里输入更多的端口映射配置 + ] +} +``` + +### 运行 PyFrp + +#### 启动服务器: +```bash +python server.py server_config.json +``` + +#### 启动客户端: +```bash +python client.py client_config.json +``` + +你也可以直接修改源代码中的默认配置。 + +--- + +## � 使用示例 + +### 示例:暴露本地 HTTP 服务器 + +1. **启动本地 HTTP 服务器**: + ```bash + python -m http.server 36667 + ``` + +2. **配置 PyFrp 客户端** (`client_config.json`): + ```json + { + "ServerDomain": "127.0.0.1", + "ServerPort": 5000, + "Key": "07A36AEF1907843", + "Forwards": [ + { + "forward_domain": "127.0.0.1", + "forward_port": 36667, + "target_port": 5002, + "mode": "TCP" + } + ] + } + ``` + +3. **启动 PyFrp 服务器和客户端**: + ```bash + # 在终端 1 中 + python server.py + + # 在终端 2 中 + python client.py + ``` + +4. **通过代理访问本地服务器**: + ```bash + curl http://localhost:5002 + ``` + + 你应该会看到来自本地服务器的 HTTP 响应,表明端口转发工作正常。 + +--- + +## ⚠️ 限制 + +- **仅支持 TCP 协议**(不支持 UDP、HTTP、HTTPS) +- **数据传输无 SSL 加密** +- **简单的认证机制**(固定密钥) +- **网络中断后无自动重连** +- **有限的错误处理**和日志记录 +- **性能限制**(由于使用 JSON 和 hex 编码传输数据) + +--- + +## 🛠️ 工作原理 + +### 服务器端 (server.py) +1. 在内部数据端口上监听客户端连接 +2. 处理客户端认证 +3. 创建和管理端口转发服务 +4. 在外部连接和客户端之间转发数据 + +### 客户端 (client.py) +1. 连接到服务器并进行认证 +2. 发送端口转发配置 +3. 处理到本地服务的连接 +4. 在服务器和本地服务之间转发数据 + +--- + +## 🤝 贡献 + +欢迎贡献!你可以通过以下方式帮助我们: + +1. **报告错误**:通过打开 issue +2. **建议功能**:通过打开 issue +3. **提交代码**:通过 pull request 提交 bug 修复或增强功能 + +### 开发指南 +- 遵循现有的代码风格 +- 为复杂代码添加注释 +- 彻底测试你的更改 +- 根据需要更新文档 + +--- + +## �📞 联系我们 + - 📧 Email: wyt18222152539wyt@163.com + - 🌐 官网: [银河万通软件开发工作室](https://www.gvsds.com) + - 📱 微信: GVSADS + +--- + +## 📄 许可证 + +本项目采用 MIT 许可证。详情请参阅 LICENSE 文件。 diff --git a/server.py b/server.py new file mode 100644 index 0000000..1e8624d --- /dev/null +++ b/server.py @@ -0,0 +1,343 @@ +import socket +import threading +import json +import traceback +import sys +import re +from collections import defaultdict + +class PortForwardServer: + def __init__(self, InternalDataPort=5000, AllowedPortRange="5001-5500", MaxPortsPerClient=5, Key="07A36AEF1907843"): + self.InternalDataPort = InternalDataPort + self.AllowedPortRange = AllowedPortRange + self.MaxPortsPerClient = MaxPortsPerClient + self.Key = Key + self.ParsePortRange() + self.ServerSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.ServerSocket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + self.Clients = {} + self.ClientLocks = defaultdict(threading.Lock) + self.ForwardMap = {} + self.ForwardLocks = defaultdict(threading.Lock) + self.Running = True + self.MessageSeparator = b'|||' + + def ParsePortRange(self): + match = re.match(r'^(\d+)-(\d+)$', self.AllowedPortRange) + if not match: + raise ValueError("Invalid port range format") + self.MinPort = int(match.group(1)) + self.MaxPort = int(match.group(2)) + if self.MinPort >= self.MaxPort or self.MinPort < 1 or self.MaxPort > 65535: + raise ValueError("Invalid port range values") + + def IsPortAllowed(self, port): + return self.MinPort <= port <= self.MaxPort + + def Start(self): + try: + self.ServerSocket.bind(('0.0.0.0', self.InternalDataPort)) + self.ServerSocket.listen(5) + print(f"Server started on port {self.InternalDataPort}") + acceptThread = threading.Thread(target=self.AcceptClients, daemon=True) + acceptThread.start() + while self.Running: + cmd = input("Enter 'exit' to stop server: ") + if cmd.lower() == 'exit': + self.Running = False + break + self.Stop() + except Exception as e: + print(f"Server start error: {e}") + traceback.print_exc() + + def Stop(self): + self.Running = False + self.ServerSocket.close() + for clientId, clientData in self.Clients.items(): + with self.ClientLocks[clientId]: + if clientData['socket']: + try: + clientData['socket'].close() + except: + pass + with self.ForwardLocks[clientId]: + for forwardId, forwardData in clientData['forwards'].items(): + try: + forwardData['server'].close() + except: + pass + print("Server stopped") + + def AcceptClients(self): + while self.Running: + try: + clientSocket, addr = self.ServerSocket.accept() + print(f"New client connection from {addr}") + clientThread = threading.Thread(target=self.HandleClient, args=(clientSocket, addr), daemon=True) + clientThread.start() + except Exception as e: + if self.Running: + print(f"Accept error: {e}") + traceback.print_exc() + + def HandleClient(self, clientSocket, addr): + clientId = f"{addr[0]}:{addr[1]}" + self.Clients[clientId] = {'socket': clientSocket, 'forwards': {}, 'addr': addr, 'buffer': b''} + try: + while self.Running: + clientSocket.settimeout(30) + try: + data = clientSocket.recv(4096) + if not data: + print(f"Client {clientId} disconnected") + break + self.Clients[clientId]['buffer'] += data + self.ProcessBuffer(clientId) + except socket.timeout: + continue + except Exception as e: + print(f"Client communication error: {e}") + traceback.print_exc() + break + finally: + with self.ClientLocks[clientId]: + if clientId in self.Clients: + del self.Clients[clientId] + with self.ForwardLocks[clientId]: + for forwardId in list(self.ForwardMap.keys()): + if forwardId.startswith(clientId): + del self.ForwardMap[forwardId] + try: + clientSocket.close() + except: + pass + print(f"Client {clientId} handler cleaned up") + + def ProcessBuffer(self, clientId): + clientData = self.Clients.get(clientId) + if not clientData: + return + while self.MessageSeparator in clientData['buffer']: + msgEnd = clientData['buffer'].index(self.MessageSeparator) + messageData = clientData['buffer'][:msgEnd] + clientData['buffer'] = clientData['buffer'][msgEnd + len(self.MessageSeparator):] + try: + message = json.loads(messageData.decode('utf-8')) + self.ProcessClientMessage(clientId, message) + except json.JSONDecodeError: + print(f"Invalid JSON from client {clientId}") + self.SendToClient(clientId, {'type': 'error', 'message': 'Invalid JSON'}) + except Exception as e: + print(f"Error processing message: {e}") + traceback.print_exc() + + def ProcessClientMessage(self, clientId, message): + if message.get('type') == 'auth': + self.HandleAuth(clientId, message) + elif message.get('type') == 'forward_request': + self.HandleForwardRequest(clientId, message) + elif message.get('type') == 'data': + self.HandleData(clientId, message) + elif message.get('type') == 'close_forward': + self.HandleCloseForward(clientId, message) + else: + self.SendToClient(clientId, {'type': 'error', 'message': 'Unknown message type'}) + + def HandleAuth(self, clientId, message): + clientData = self.Clients.get(clientId) + if not clientData: + return + if message.get('key') == self.Key: + clientData['authenticated'] = True + self.SendToClient(clientId, {'type': 'auth_response', 'success': True}) + print(f"Client {clientId} authenticated successfully") + else: + self.SendToClient(clientId, {'type': 'auth_response', 'success': False, 'message': 'Invalid key'}) + clientData['socket'].close() + print(f"Client {clientId} failed authentication") + + def HandleForwardRequest(self, clientId, message): + clientData = self.Clients.get(clientId) + if not clientData or not clientData.get('authenticated', False): + self.SendToClient(clientId, {'type': 'forward_response', 'success': False, 'message': 'Not authenticated'}) + return + with self.ClientLocks[clientId]: + if len(clientData['forwards']) >= self.MaxPortsPerClient: + self.SendToClient(clientId, {'type': 'forward_response', 'success': False, 'message': 'Max ports per client reached'}) + return + targetPort = message.get('target_port') + mode = message.get('mode', 'tcp').upper() + if not self.IsPortAllowed(targetPort): + self.SendToClient(clientId, {'type': 'forward_response', 'success': False, 'message': 'Target port not allowed'}) + return + forwardId = f"{clientId}:{targetPort}" + if forwardId in self.ForwardMap: + self.SendToClient(clientId, {'type': 'forward_response', 'success': False, 'message': 'Port already in use'}) + return + try: + if mode == 'TCP': + forwardServer = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + forwardServer.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + forwardServer.bind(('0.0.0.0', targetPort)) + forwardServer.listen(5) + with self.ClientLocks[clientId]: + clientData['forwards'][forwardId] = {'server': forwardServer, 'mode': mode, 'connections': {}} + with self.ForwardLocks[clientId]: + self.ForwardMap[forwardId] = clientId + threading.Thread(target=self.AcceptForwardConnections, args=(clientId, forwardId, forwardServer), daemon=True).start() + self.SendToClient(clientId, {'type': 'forward_response', 'success': True, 'target_port': targetPort, 'forward_id': forwardId}) + print(f"Forward created: {forwardId}") + else: + self.SendToClient(clientId, {'type': 'forward_response', 'success': False, 'message': 'Unsupported mode'}) + except Exception as e: + print(f"Forward creation error: {e}") + traceback.print_exc() + self.SendToClient(clientId, {'type': 'forward_response', 'success': False, 'message': str(e)}) + + def AcceptForwardConnections(self, clientId, forwardId, forwardServer): + try: + while self.Running and forwardId in self.ForwardMap and self.ForwardMap[forwardId] == clientId: + forwardServer.settimeout(1) + try: + conn, addr = forwardServer.accept() + connId = f"{addr[0]}:{addr[1]}" + print(f"New connection to forward {forwardId} from {connId}") + with self.ClientLocks[clientId]: + clientData = self.Clients.get(clientId) + if not clientData or forwardId not in clientData['forwards']: + conn.close() + continue + clientData['forwards'][forwardId]['connections'][connId] = conn + self.SendToClient(clientId, { + 'type': 'new_connection', + 'forward_id': forwardId, + 'conn_id': connId + }) + threading.Thread(target=self.ForwardToClient, args=(clientId, forwardId, connId, conn), daemon=True).start() + except socket.timeout: + continue + except Exception as e: + print(f"Forward accept error: {e}") + traceback.print_exc() + finally: + try: + forwardServer.close() + except: + pass + print(f"Forward listener {forwardId} stopped") + + def ForwardToClient(self, clientId, forwardId, connId, conn): + try: + while self.Running: + conn.settimeout(1) + try: + data = conn.recv(4096) + if not data: + break + self.SendToClient(clientId, { + 'type': 'data', + 'forward_id': forwardId, + 'conn_id': connId, + 'data': data.hex() + }) + except socket.timeout: + if not self.Running or clientId not in self.Clients: + break + continue + except Exception as e: + print(f"Forward to client error: {e}") + traceback.print_exc() + break + finally: + try: + conn.close() + except: + pass + with self.ClientLocks[clientId]: + clientData = self.Clients.get(clientId) + if clientData and forwardId in clientData['forwards']: + if connId in clientData['forwards'][forwardId]['connections']: + del clientData['forwards'][forwardId]['connections'][connId] + self.SendToClient(clientId, { + 'type': 'close_connection', + 'forward_id': forwardId, + 'conn_id': connId + }) + print(f"Connection {connId} to forward {forwardId} closed") + + def HandleData(self, clientId, message): + forwardId = message.get('forward_id') + connId = message.get('conn_id') + dataHex = message.get('data') + if not all([forwardId, connId, dataHex]): + return + try: + data = bytes.fromhex(dataHex) + with self.ClientLocks[clientId]: + clientData = self.Clients.get(clientId) + if not clientData or forwardId not in clientData['forwards']: + return + forwardData = clientData['forwards'][forwardId] + if connId not in forwardData['connections']: + return + conn = forwardData['connections'][connId] + conn.sendall(data) + except Exception as e: + print(f"Data handling error: {e}") + traceback.print_exc() + + def HandleCloseForward(self, clientId, message): + forwardId = message.get('forward_id') + if not forwardId: + return + with self.ClientLocks[clientId]: + clientData = self.Clients.get(clientId) + if clientData and forwardId in clientData['forwards']: + try: + clientData['forwards'][forwardId]['server'].close() + except: + pass + del clientData['forwards'][forwardId] + with self.ForwardLocks[clientId]: + if forwardId in self.ForwardMap: + del self.ForwardMap[forwardId] + print(f"Forward {forwardId} closed by client") + + def SendToClient(self, clientId, message): + try: + with self.ClientLocks[clientId]: + clientData = self.Clients.get(clientId) + if not clientData or not clientData['socket']: + return + clientSocket = clientData['socket'] + data = json.dumps(message).encode('utf-8') + self.MessageSeparator + clientSocket.sendall(data) + except Exception as e: + print(f"Error sending to client {clientId}: {e}") + traceback.print_exc() + +def main(): + config = { + "InternalDataPort": 5000, + "AllowedPortRange": "5001-5500", + "MaxPortsPerClient": 5, + "Key": "07A36AEF1907843" + } + if len(sys.argv) > 1: + try: + with open(sys.argv[1], 'r') as f: + config.update(json.load(f)) + except Exception as e: + print(f"Error loading config file: {e}") + traceback.print_exc() + server = PortForwardServer( + InternalDataPort=int(config["InternalDataPort"]), + AllowedPortRange=config["AllowedPortRange"], + MaxPortsPerClient=int(config["MaxPortsPerClient"]), + Key=config["Key"] + ) + server.Start() + +if __name__ == "__main__": + main()