commit 9cee40db866da58da882b851d7c1b12f15faa36a Author: GVSADS Date: Thu Jan 29 22:53:58 2026 +0800 Initial commit of PyFrp project 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 0000000..aa5d030 Binary files /dev/null and b/__pycache__/__init__.cpython-312.pyc differ diff --git a/__pycache__/client.cpython-312.pyc b/__pycache__/client.cpython-312.pyc new file mode 100644 index 0000000..28a695d Binary files /dev/null and b/__pycache__/client.cpython-312.pyc differ diff --git a/__pycache__/server.cpython-312.pyc b/__pycache__/server.cpython-312.pyc new file mode 100644 index 0000000..caaad1e Binary files /dev/null and b/__pycache__/server.cpython-312.pyc differ diff --git a/backup/__init__.py b/backup/__init__.py new file mode 100644 index 0000000..bf6328b --- /dev/null +++ b/backup/__init__.py @@ -0,0 +1,7 @@ +# -*- 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. + +from . import client +from . import server diff --git a/backup/client.py b/backup/client.py new file mode 100644 index 0000000..c00535a --- /dev/null +++ b/backup/client.py @@ -0,0 +1,208 @@ +# -*- 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 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 PortMapping: + def __init__(self, ForwardHost, ForwardPort, TargetPort, Mode): + self.ForwardHost = ForwardHost + self.ForwardPort = ForwardPort + self.TargetPort = TargetPort + self.Mode = Mode.lower() + self.IsActive = False + def ToDict(self): + return { + 'forward_host': self.ForwardHost, + 'forward_port': self.ForwardPort, + 'target_port': self.TargetPort, + 'mode': self.Mode + } + +class PortForwardClient: + def __init__(self, ServerHost="127.0.0.1", ServerPort=5000, Key="07A36AEF1907843"): + self.ServerHost = ServerHost + self.ServerPort = ServerPort + self.Key = Key + self.Socket = None + self.IsRunning = False + self.Mappings = [] + self.MappingsLock = threading.Lock() + self.Encryptor = AesEncryptor(Key) + def Log(self, Message): + Timestamp = time.strftime('%Y-%m-%d %H:%M:%S') + print(f"[{Timestamp}] [Client] {Message}") + def HandleError(self, Message): + self.Log(f"Error: {Message}") + self.Log(traceback.format_exc()) + def Connect(self): + try: + self.Socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.Socket.connect((self.ServerHost, self.ServerPort)) + self.Log(f"Connected to server {self.ServerHost}:{self.ServerPort}") + return True + except Exception as E: + self.HandleError(f"Connection failed: {str(E)}") + self.Socket = None + return False + def AddMapping(self, ForwardHost, ForwardPort, TargetPort, Mode): + with self.MappingsLock: + for M in self.Mappings: + if M.TargetPort == TargetPort: + self.Log(f"Mapping for target port {TargetPort} already exists") + return False + Mapping = PortMapping(ForwardHost, ForwardPort, TargetPort, Mode) + self.Mappings.append(Mapping) + # 仅当已连接时才立即注册映射 + if self.Socket is not None: + return self.RegisterMapping(Mapping) + return True # 连接后会自动注册 + def RegisterMapping(self, Mapping): + if self.Socket is None: + self.Log("Cannot register mapping - not connected to server") + return False + try: + Command = { + 'type': 'register', + 'timestamp': str(time.time()), + **Mapping.ToDict() + } + 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 registered mapping: {Mapping.TargetPort} -> {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()