py-mtproxy-lib 0.0.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-mtproxy-lib
3
+ Version: 0.0.2
4
+ Summary: Production-ready MTProto proxy server library with multi-secret support, statistics, and daemon mode
5
+ Home-page: https://github.com/twosleepynights0x1/py-mtproxy-lib
6
+ Author: Your Name
7
+ Author-email: tashova28@gmail.com
8
+ Project-URL: Bug Reports, https://github.com/twosleepynights0x1/py-mtproxy-lib
9
+ Keywords: mtproto telegram proxy vpn daemon docker
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: Topic :: Internet :: Proxy Servers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.7
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: License :: OSI Approved :: MIT License
20
+ Classifier: Operating System :: POSIX
21
+ Classifier: Operating System :: Unix
22
+ Requires-Python: >=3.7
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: pycryptodome>=3.10.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=6.0; extra == "dev"
27
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
28
+ Requires-Dist: black>=22.0; extra == "dev"
29
+ Requires-Dist: flake8>=4.0; extra == "dev"
30
+ Provides-Extra: docker
31
+ Requires-Dist: docker>=6.0; extra == "docker"
32
+ Dynamic: author
33
+ Dynamic: author-email
34
+ Dynamic: classifier
35
+ Dynamic: description
36
+ Dynamic: description-content-type
37
+ Dynamic: home-page
38
+ Dynamic: keywords
39
+ Dynamic: project-url
40
+ Dynamic: provides-extra
41
+ Dynamic: requires-dist
42
+ Dynamic: requires-python
43
+ Dynamic: summary
44
+
45
+ cat > README.md << 'EOF'
46
+ # py-mtproxy-lib
47
+
48
+ Python библиотека для запуска MTProto прокси сервера для Telegram.
49
+
50
+ ## Возможности
51
+
52
+ - Поддержка нескольких секретов
53
+ - Fake TLS маскировка
54
+ - Статистика подключений и трафика
55
+ - Логирование с разными уровнями
56
+ - Запуск как системный демон
57
+ - Docker образ
58
+
@@ -0,0 +1,14 @@
1
+ cat > README.md << 'EOF'
2
+ # py-mtproxy-lib
3
+
4
+ Python библиотека для запуска MTProto прокси сервера для Telegram.
5
+
6
+ ## Возможности
7
+
8
+ - Поддержка нескольких секретов
9
+ - Fake TLS маскировка
10
+ - Статистика подключений и трафика
11
+ - Логирование с разными уровнями
12
+ - Запуск как системный демон
13
+ - Docker образ
14
+
@@ -0,0 +1,23 @@
1
+ """
2
+ py-mtproxy-lib - Python библиотека для запуска MTProto прокси
3
+ """
4
+
5
+ from .server import MTProxyServer
6
+ from .utils import generate_secret
7
+ from .stats import ProxyStats, ConnectionStats
8
+ from .logger import setup_logger
9
+ from .config import ProxyConfig
10
+ from .daemon import Daemon
11
+ from .cli import main
12
+
13
+ __version__ = "2.0.0"
14
+ __all__ = [
15
+ 'MTProxyServer',
16
+ 'generate_secret',
17
+ 'ProxyStats',
18
+ 'ConnectionStats',
19
+ 'setup_logger',
20
+ 'ProxyConfig',
21
+ 'Daemon',
22
+ 'main'
23
+ ]
@@ -0,0 +1,192 @@
1
+ """CLI интерфейс"""
2
+
3
+ import argparse
4
+ import sys
5
+ import os
6
+ from .config import ProxyConfig
7
+ from .server import MTProxyServer
8
+ from .utils import generate_secret
9
+ from .daemon import Daemon
10
+ from .logger import setup_logger
11
+
12
+
13
+ def start_server(config: ProxyConfig, foreground: bool = False):
14
+ """Запуск сервера"""
15
+ # Настройка логгера
16
+ logger = setup_logger(
17
+ level=config.log_level,
18
+ log_file=config.log_file
19
+ )
20
+
21
+ server = MTProxyServer(config)
22
+
23
+ if foreground:
24
+ server.start()
25
+ else:
26
+ daemon = Daemon(
27
+ pidfile=config.pid_file or '/var/run/mtproxy.pid',
28
+ stdout=config.log_file or '/dev/null',
29
+ stderr=config.log_file or '/dev/null'
30
+ )
31
+ daemon.start(server.start)
32
+
33
+
34
+ def main():
35
+ """Основная функция CLI"""
36
+ parser = argparse.ArgumentParser(
37
+ description="MTProto Proxy Server",
38
+ prog="mtproxy"
39
+ )
40
+
41
+ parser.add_argument(
42
+ "-c", "--config",
43
+ help="Config file path",
44
+ type=str,
45
+ default="mtproxy.conf"
46
+ )
47
+
48
+ parser.add_argument(
49
+ "-s", "--secret",
50
+ help="Add secret (can be used multiple times)",
51
+ action="append"
52
+ )
53
+
54
+ parser.add_argument(
55
+ "-p", "--port",
56
+ help="Port to listen on",
57
+ type=int
58
+ )
59
+
60
+ parser.add_argument(
61
+ "--no-fake-tls",
62
+ help="Disable Fake TLS masking",
63
+ action="store_true"
64
+ )
65
+
66
+ parser.add_argument(
67
+ "-d", "--daemon",
68
+ help="Run as daemon",
69
+ action="store_true"
70
+ )
71
+
72
+ parser.add_argument(
73
+ "--pid-file",
74
+ help="PID file path",
75
+ type=str
76
+ )
77
+
78
+ parser.add_argument(
79
+ "--log-file",
80
+ help="Log file path",
81
+ type=str
82
+ )
83
+
84
+ parser.add_argument(
85
+ "--log-level",
86
+ help="Log level",
87
+ choices=['DEBUG', 'INFO', 'WARNING', 'ERROR'],
88
+ default='INFO'
89
+ )
90
+
91
+ parser.add_argument(
92
+ "--stats-interval",
93
+ help="Statistics output interval (seconds)",
94
+ type=int
95
+ )
96
+
97
+ parser.add_argument(
98
+ "--generate-secret",
99
+ help="Generate random secret and exit",
100
+ action="store_true"
101
+ )
102
+
103
+ parser.add_argument(
104
+ "--show-links",
105
+ help="Show connection links",
106
+ action="store_true"
107
+ )
108
+
109
+ # Команды управления демоном
110
+ subparsers = parser.add_subparsers(dest="command", help="Daemon commands")
111
+
112
+ subparsers.add_parser("start", help="Start daemon")
113
+ subparsers.add_parser("stop", help="Stop daemon")
114
+ subparsers.add_parser("restart", help="Restart daemon")
115
+ subparsers.add_parser("status", help="Check daemon status")
116
+
117
+ args = parser.parse_args()
118
+
119
+ # Генерация секрета
120
+ if args.generate_secret:
121
+ print(generate_secret())
122
+ sys.exit(0)
123
+
124
+ # Загрузка конфигурации
125
+ config = None
126
+ if os.path.exists(args.config):
127
+ try:
128
+ config = ProxyConfig.from_file(args.config)
129
+ except Exception as e:
130
+ print(f"Error loading config: {e}", file=sys.stderr)
131
+
132
+ if not config:
133
+ config = ProxyConfig()
134
+
135
+ # Применение CLI аргументов
136
+ if args.port:
137
+ config.port = args.port
138
+ if args.no_fake_tls:
139
+ config.enable_fake_tls = False
140
+ if args.pid_file:
141
+ config.pid_file = args.pid_file
142
+ if args.log_file:
143
+ config.log_file = args.log_file
144
+ if args.log_level:
145
+ config.log_level = args.log_level
146
+ if args.stats_interval:
147
+ config.stats_interval = args.stats_interval
148
+
149
+ # Добавление секретов
150
+ if args.secret:
151
+ for secret in args.secret:
152
+ config.add_secret(secret)
153
+
154
+ # Если нет секретов, создаем один
155
+ if not config.secrets:
156
+ config.add_secret()
157
+ print(f"Generated default secret: {list(config.secrets.keys())[0]}")
158
+
159
+ # Показ ссылок
160
+ if args.show_links:
161
+ links = config.get_connection_links()
162
+ print("Connection links:")
163
+ for name, link in links.items():
164
+ print(f" {name}: {link}")
165
+ sys.exit(0)
166
+
167
+ # Команды демона
168
+ daemon = Daemon(
169
+ pidfile=config.pid_file or '/var/run/mtproxy.pid',
170
+ stdout=config.log_file or '/dev/null',
171
+ stderr=config.log_file or '/dev/null'
172
+ )
173
+
174
+ if args.command == "start":
175
+ sys.exit(daemon.start(start_server, config))
176
+ elif args.command == "stop":
177
+ sys.exit(daemon.stop())
178
+ elif args.command == "restart":
179
+ sys.exit(daemon.restart(start_server, config))
180
+ elif args.command == "status":
181
+ print(daemon.status())
182
+ sys.exit(0)
183
+
184
+ # Запуск в foreground или daemon
185
+ if args.daemon:
186
+ daemon.start(start_server, config)
187
+ else:
188
+ start_server(config, foreground=True)
189
+
190
+
191
+ if __name__ == "__main__":
192
+ main()
@@ -0,0 +1,161 @@
1
+ """Конфигурация с поддержкой нескольких секретов"""
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass, field
6
+ from typing import Dict, List, Optional, Tuple
7
+ from .utils import generate_secret, verify_fake_tls_handshake
8
+
9
+
10
+ @dataclass
11
+ class ProxyConfig:
12
+ """
13
+ Конфигурация прокси сервера с поддержкой нескольких секретов
14
+
15
+ Attributes:
16
+ secrets (Dict[str, str]): Словарь {secret: tag}
17
+ port (int): Порт для прослушивания
18
+ enable_fake_tls (bool): Включить Fake TLS маскировку
19
+ handshake_timeout (int): Таймаут handshake в секундах
20
+ stats_interval (int): Интервал вывода статистики (сек)
21
+ log_level (str): Уровень логирования
22
+ log_file (Optional[str]): Файл лога
23
+ pid_file (Optional[str]): Файл PID
24
+ """
25
+
26
+ secrets: Dict[str, str] = field(default_factory=dict)
27
+ port: int = 443
28
+ enable_fake_tls: bool = True
29
+ handshake_timeout: int = 10
30
+ stats_interval: int = 60
31
+ log_level: str = "INFO"
32
+ log_file: Optional[str] = None
33
+ pid_file: Optional[str] = None
34
+
35
+ @classmethod
36
+ def from_dict(cls, data: dict) -> 'ProxyConfig':
37
+ """Создание конфигурации из словаря"""
38
+ secrets = {}
39
+
40
+ # Поддержка разных форматов
41
+ if 'secrets' in data:
42
+ if isinstance(data['secrets'], list):
43
+ for item in data['secrets']:
44
+ if isinstance(item, str):
45
+ secrets[item] = ""
46
+ elif isinstance(item, dict):
47
+ secrets[item.get('secret', '')] = item.get('tag', '')
48
+ elif isinstance(data['secrets'], dict):
49
+ secrets = data['secrets']
50
+
51
+ return cls(
52
+ secrets=secrets,
53
+ port=data.get('port', 443),
54
+ enable_fake_tls=data.get('enable_fake_tls', True),
55
+ handshake_timeout=data.get('handshake_timeout', 10),
56
+ stats_interval=data.get('stats_interval', 60),
57
+ log_level=data.get('log_level', 'INFO'),
58
+ log_file=data.get('log_file'),
59
+ pid_file=data.get('pid_file')
60
+ )
61
+
62
+ @classmethod
63
+ def from_file(cls, path: str) -> 'ProxyConfig':
64
+ """Загрузка конфигурации из файла"""
65
+ if not os.path.exists(path):
66
+ raise FileNotFoundError(f"Config file not found: {path}")
67
+
68
+ with open(path, 'r') as f:
69
+ if path.endswith('.json'):
70
+ data = json.load(f)
71
+ else:
72
+ # INI-like format
73
+ data = cls._parse_ini(f.read())
74
+
75
+ return cls.from_dict(data)
76
+
77
+ @classmethod
78
+ def _parse_ini(cls, content: str) -> dict:
79
+ """Парсинг INI-подобного формата"""
80
+ data = {'secrets': {}}
81
+
82
+ for line in content.split('\n'):
83
+ line = line.strip()
84
+ if not line or line.startswith('#'):
85
+ continue
86
+
87
+ if '=' in line:
88
+ key, value = line.split('=', 1)
89
+ key = key.strip()
90
+ value = value.strip()
91
+
92
+ if key == 'secret':
93
+ # Простой формат: secret=xxx
94
+ data['secrets'][value] = ""
95
+ elif key.startswith('secret_'):
96
+ # Формат: secret_name=xxx
97
+ tag = key.replace('secret_', '')
98
+ data['secrets'][value] = tag
99
+ else:
100
+ # Другие параметры
101
+ data[key] = value
102
+
103
+ return data
104
+
105
+ def save(self, path: str) -> None:
106
+ """Сохранение конфигурации в файл"""
107
+ data = {
108
+ 'port': self.port,
109
+ 'enable_fake_tls': self.enable_fake_tls,
110
+ 'handshake_timeout': self.handshake_timeout,
111
+ 'stats_interval': self.stats_interval,
112
+ 'log_level': self.log_level,
113
+ 'secrets': self.secrets
114
+ }
115
+
116
+ with open(path, 'w') as f:
117
+ json.dump(data, f, indent=2)
118
+
119
+ def add_secret(self, secret: Optional[str] = None, tag: str = "") -> str:
120
+ """Добавление нового секрета"""
121
+ if not secret:
122
+ secret = generate_secret()
123
+
124
+ self.secrets[secret] = tag
125
+ return secret
126
+
127
+ def remove_secret(self, secret: str) -> bool:
128
+ """Удаление секрета"""
129
+ if secret in self.secrets:
130
+ del self.secrets[secret]
131
+ return True
132
+ return False
133
+
134
+ def verify_secret(self, data: bytes) -> Tuple[bool, Optional[str]]:
135
+ """
136
+ Проверка handshake и определение использованного секрета
137
+
138
+ Returns:
139
+ Tuple[bool, Optional[str]]: (успех, секрет)
140
+ """
141
+ for secret_hex, tag in self.secrets.items():
142
+ secret = bytes.fromhex(secret_hex)
143
+ if verify_fake_tls_handshake(data, secret):
144
+ return True, secret_hex
145
+
146
+ return False, None
147
+
148
+ def get_connection_links(self, base_ip: Optional[str] = None) -> Dict[str, str]:
149
+ """Получение ссылок для всех секретов"""
150
+ from .utils import get_external_ip
151
+
152
+ ip = base_ip or get_external_ip() or "YOUR_SERVER_IP"
153
+ links = {}
154
+
155
+ for secret, tag in self.secrets.items():
156
+ link = f"tg://proxy?server={ip}&port={self.port}&secret={secret}"
157
+ if tag:
158
+ link += f"&tag={tag}"
159
+ links[secret[:16] + ("..." if len(secret) > 16 else "")] = link
160
+
161
+ return links
@@ -0,0 +1,192 @@
1
+ """Модуль для запуска как системный демон"""
2
+
3
+ import os
4
+ import sys
5
+ import atexit
6
+ import signal
7
+ import time
8
+ from typing import Optional, Callable
9
+
10
+
11
+ class Daemon:
12
+ """
13
+ Класс для управления демоном
14
+
15
+ Пример использования:
16
+ daemon = Daemon(pidfile='/var/run/mtproxy.pid')
17
+ daemon.start(run_function)
18
+ """
19
+
20
+ def __init__(self, pidfile: str, stdin: str = '/dev/null',
21
+ stdout: str = '/dev/null', stderr: str = '/dev/null'):
22
+ """
23
+ Инициализация демона
24
+
25
+ Args:
26
+ pidfile: Путь к файлу PID
27
+ stdin: Перенаправление stdin
28
+ stdout: Перенаправление stdout
29
+ stderr: Перенаправление stderr
30
+ """
31
+ self.pidfile = pidfile
32
+ self.stdin = stdin
33
+ self.stdout = stdout
34
+ self.stderr = stderr
35
+ self._running = False
36
+
37
+ def _daemonize(self) -> None:
38
+ """Демонизация процесса (двойной fork)"""
39
+ try:
40
+ pid = os.fork()
41
+ if pid > 0:
42
+ sys.exit(0)
43
+ except OSError as e:
44
+ sys.stderr.write(f"Fork #1 failed: {e}\n")
45
+ sys.exit(1)
46
+
47
+ # Отсоединение от терминала
48
+ os.setsid()
49
+ os.umask(0)
50
+
51
+ try:
52
+ pid = os.fork()
53
+ if pid > 0:
54
+ sys.exit(0)
55
+ except OSError as e:
56
+ sys.stderr.write(f"Fork #2 failed: {e}\n")
57
+ sys.exit(1)
58
+
59
+ # Перенаправление файловых дескрипторов
60
+ sys.stdout.flush()
61
+ sys.stderr.flush()
62
+
63
+ with open(self.stdin, 'r') as si:
64
+ os.dup2(si.fileno(), sys.stdin.fileno())
65
+ with open(self.stdout, 'a+') as so:
66
+ os.dup2(so.fileno(), sys.stdout.fileno())
67
+ with open(self.stderr, 'a+') as se:
68
+ os.dup2(se.fileno(), sys.stderr.fileno())
69
+
70
+ # Запись PID
71
+ self._write_pidfile()
72
+
73
+ # Регистрация удаления PID файла при выходе
74
+ atexit.register(self._remove_pidfile)
75
+
76
+ # Обработка сигналов
77
+ signal.signal(signal.SIGTERM, self._signal_handler)
78
+ signal.signal(signal.SIGINT, self._signal_handler)
79
+
80
+ def _write_pidfile(self) -> None:
81
+ """Запись PID в файл"""
82
+ with open(self.pidfile, 'w') as f:
83
+ f.write(str(os.getpid()))
84
+
85
+ def _remove_pidfile(self) -> None:
86
+ """Удаление PID файла"""
87
+ if os.path.exists(self.pidfile):
88
+ os.remove(self.pidfile)
89
+
90
+ def _signal_handler(self, signum, frame) -> None:
91
+ """Обработчик сигналов"""
92
+ self._running = False
93
+
94
+ def _get_pid(self) -> Optional[int]:
95
+ """Получение PID из файла"""
96
+ try:
97
+ with open(self.pidfile, 'r') as f:
98
+ return int(f.read().strip())
99
+ except (IOError, ValueError):
100
+ return None
101
+
102
+ def start(self, run_func: Callable, *args, **kwargs) -> int:
103
+ """
104
+ Запуск демона
105
+
106
+ Args:
107
+ run_func: Функция для запуска
108
+ *args, **kwargs: Аргументы для функции
109
+
110
+ Returns:
111
+ int: 0 при успехе, 1 при ошибке
112
+ """
113
+ pid = self._get_pid()
114
+
115
+ if pid:
116
+ # Проверка, работает ли процесс
117
+ try:
118
+ os.kill(pid, 0)
119
+ sys.stderr.write(f"Daemon already running with PID {pid}\n")
120
+ return 1
121
+ except OSError:
122
+ # Процесс не работает, удаляем старый PID файл
123
+ self._remove_pidfile()
124
+
125
+ # Демонизация
126
+ self._daemonize()
127
+
128
+ # Запуск основной функции
129
+ self._running = True
130
+ try:
131
+ run_func(*args, **kwargs)
132
+ except KeyboardInterrupt:
133
+ pass
134
+ except Exception as e:
135
+ sys.stderr.write(f"Error in daemon: {e}\n")
136
+ return 1
137
+
138
+ return 0
139
+
140
+ def stop(self) -> int:
141
+ """Остановка демона"""
142
+ pid = self._get_pid()
143
+
144
+ if not pid:
145
+ sys.stderr.write("Daemon not running\n")
146
+ return 1
147
+
148
+ try:
149
+ os.kill(pid, signal.SIGTERM)
150
+
151
+ # Ожидание завершения
152
+ timeout = 10
153
+ while timeout > 0:
154
+ try:
155
+ os.kill(pid, 0)
156
+ time.sleep(0.5)
157
+ timeout -= 0.5
158
+ except OSError:
159
+ break
160
+
161
+ # Принудительное завершение
162
+ try:
163
+ os.kill(pid, 0)
164
+ os.kill(pid, signal.SIGKILL)
165
+ except OSError:
166
+ pass
167
+
168
+ self._remove_pidfile()
169
+
170
+ except OSError as e:
171
+ sys.stderr.write(f"Error stopping daemon: {e}\n")
172
+ return 1
173
+
174
+ return 0
175
+
176
+ def restart(self, run_func: Callable, *args, **kwargs) -> int:
177
+ """Перезапуск демона"""
178
+ self.stop()
179
+ return self.start(run_func, *args, **kwargs)
180
+
181
+ def status(self) -> str:
182
+ """Проверка статуса демона"""
183
+ pid = self._get_pid()
184
+
185
+ if not pid:
186
+ return "stopped"
187
+
188
+ try:
189
+ os.kill(pid, 0)
190
+ return f"running (PID: {pid})"
191
+ except OSError:
192
+ return "stopped (stale PID file)"
@@ -0,0 +1,102 @@
1
+ """Модуль логирования"""
2
+
3
+ import logging
4
+ import sys
5
+ from typing import Optional
6
+ from logging.handlers import RotatingFileHandler
7
+
8
+
9
+ class ColoredFormatter(logging.Formatter):
10
+ """Форматирование с цветами для консоли"""
11
+
12
+ COLORS = {
13
+ 'DEBUG': '\033[36m', # Cyan
14
+ 'INFO': '\033[32m', # Green
15
+ 'WARNING': '\033[33m', # Yellow
16
+ 'ERROR': '\033[31m', # Red
17
+ 'CRITICAL': '\033[35m', # Magenta
18
+ }
19
+ RESET = '\033[0m'
20
+
21
+ def format(self, record):
22
+ levelname = record.levelname
23
+ if levelname in self.COLORS:
24
+ record.levelname = f"{self.COLORS[levelname]}{levelname}{self.RESET}"
25
+ return super().format(record)
26
+
27
+
28
+ def setup_logger(
29
+ name: str = "mtproxy",
30
+ level: str = "INFO",
31
+ log_file: Optional[str] = None,
32
+ console: bool = True,
33
+ max_bytes: int = 10 * 1024 * 1024, # 10 MB
34
+ backup_count: int = 5
35
+ ) -> logging.Logger:
36
+ """
37
+ Настройка логгера
38
+
39
+ Args:
40
+ name: Имя логгера
41
+ level: Уровень логирования (DEBUG, INFO, WARNING, ERROR)
42
+ log_file: Путь к файлу лога (опционально)
43
+ console: Вывод в консоль
44
+ max_bytes: Максимальный размер файла лога
45
+ backup_count: Количество резервных копий
46
+
47
+ Returns:
48
+ logging.Logger: Настроенный логгер
49
+ """
50
+ logger = logging.getLogger(name)
51
+
52
+ # Очистка существующих обработчиков
53
+ logger.handlers.clear()
54
+
55
+ # Установка уровня
56
+ level_map = {
57
+ 'DEBUG': logging.DEBUG,
58
+ 'INFO': logging.INFO,
59
+ 'WARNING': logging.WARNING,
60
+ 'ERROR': logging.ERROR,
61
+ 'CRITICAL': logging.CRITICAL
62
+ }
63
+ logger.setLevel(level_map.get(level.upper(), logging.INFO))
64
+
65
+ # Формат логов
66
+ formatter = logging.Formatter(
67
+ '%(asctime)s - %(name)s - %(levelname)s - %(message)s',
68
+ datefmt='%Y-%m-%d %H:%M:%S'
69
+ )
70
+
71
+ # Консольный вывод
72
+ if console:
73
+ console_handler = logging.StreamHandler(sys.stdout)
74
+ console_handler.setFormatter(ColoredFormatter(
75
+ '%(asctime)s - %(levelname)s - %(message)s',
76
+ datefmt='%H:%M:%S'
77
+ ))
78
+ logger.addHandler(console_handler)
79
+
80
+ # Файловый вывод
81
+ if log_file:
82
+ file_handler = RotatingFileHandler(
83
+ log_file,
84
+ maxBytes=max_bytes,
85
+ backupCount=backup_count
86
+ )
87
+ file_handler.setFormatter(formatter)
88
+ logger.addHandler(file_handler)
89
+
90
+ return logger
91
+
92
+
93
+ # Глобальный логгер
94
+ _default_logger = None
95
+
96
+
97
+ def get_logger() -> logging.Logger:
98
+ """Получение глобального логгера"""
99
+ global _default_logger
100
+ if _default_logger is None:
101
+ _default_logger = setup_logger()
102
+ return _default_logger
@@ -0,0 +1,235 @@
1
+ """Основной класс MTProto прокси сервера"""
2
+
3
+ import asyncio
4
+ import uuid
5
+ import signal
6
+ import time
7
+ from typing import Optional, Dict, Callable
8
+ from .config import ProxyConfig
9
+ from .stats import ProxyStats
10
+ from .logger import get_logger
11
+ from .utils import verify_fake_tls_handshake, get_external_ip
12
+
13
+
14
+ class MTProxyServer:
15
+ """
16
+ MTProto прокси сервер с поддержкой нескольких секретов и статистики
17
+ """
18
+
19
+ def __init__(self, config: ProxyConfig):
20
+ """
21
+ Инициализация прокси сервера
22
+
23
+ Args:
24
+ config: Конфигурация прокси
25
+ """
26
+ self.config = config
27
+ self.logger = get_logger()
28
+ self.stats = ProxyStats()
29
+ self.server: Optional[asyncio.Server] = None
30
+ self._running = False
31
+ self._tasks: Dict[str, asyncio.Task] = {}
32
+ self._conn_counter = 0
33
+
34
+ def _get_conn_id(self) -> str:
35
+ """Генерация ID для соединения"""
36
+ self._conn_counter += 1
37
+ return f"{self._conn_counter}_{uuid.uuid4().hex[:8]}"
38
+
39
+ async def _handle_connection(
40
+ self,
41
+ reader: asyncio.StreamReader,
42
+ writer: asyncio.StreamWriter
43
+ ):
44
+ """Обработка входящего подключения"""
45
+ conn_id = self._get_conn_id()
46
+ client_addr = writer.get_extra_info('peername')
47
+ client_ip = client_addr[0] if client_addr else "unknown"
48
+
49
+ try:
50
+ # Чтение handshake
51
+ handshake = await asyncio.wait_for(
52
+ reader.read(1024),
53
+ timeout=self.config.handshake_timeout
54
+ )
55
+
56
+ if not handshake:
57
+ writer.close()
58
+ return
59
+
60
+ # Аутентификация с несколькими секретами
61
+ if self.config.enable_fake_tls and handshake[0] == 0x16:
62
+ success, secret = self.config.verify_secret(handshake)
63
+ if not success:
64
+ self.logger.warning(f"Auth failed from {client_ip}")
65
+ writer.close()
66
+ return
67
+ else:
68
+ # Классический режим - проверяем первый секрет
69
+ if len(handshake) < 4 or handshake[:4] != b'\xef\xef\xef\xef':
70
+ self.logger.warning(f"Invalid protocol from {client_ip}")
71
+ writer.close()
72
+ return
73
+ secret = list(self.config.secrets.keys())[0] if self.config.secrets else None
74
+ if not secret:
75
+ writer.close()
76
+ return
77
+
78
+ self.logger.info(f"Client connected: {client_ip} (secret: {secret[:16]}...)")
79
+
80
+ # Регистрация соединения в статистике
81
+ self.stats.connection_started(conn_id, client_ip, secret)
82
+
83
+ # Подключение к Telegram
84
+ # Используем несколько IP для балансировки
85
+ tg_ips = ['149.154.167.50', '149.154.167.51', '149.154.175.100']
86
+ tg_reader, tg_writer = await asyncio.open_connection(
87
+ tg_ips[hash(secret) % len(tg_ips)],
88
+ 443
89
+ )
90
+
91
+ # Создание задач для пересылки данных
92
+ task_client = asyncio.create_task(
93
+ self._forward(reader, tg_writer, conn_id, sent=True)
94
+ )
95
+ task_server = asyncio.create_task(
96
+ self._forward(tg_reader, writer, conn_id, received=True)
97
+ )
98
+
99
+ self._tasks[conn_id] = task_client
100
+ self._tasks[f"{conn_id}_server"] = task_server
101
+
102
+ # Ожидание завершения
103
+ await asyncio.gather(task_client, task_server)
104
+
105
+ except asyncio.TimeoutError:
106
+ self.logger.debug(f"Handshake timeout from {client_ip}")
107
+ except ConnectionRefusedError:
108
+ self.logger.error(f"Connection refused to Telegram from {client_ip}")
109
+ except Exception as e:
110
+ self.logger.error(f"Error handling connection from {client_ip}: {e}")
111
+ finally:
112
+ # Завершение соединения
113
+ self.stats.connection_ended(conn_id)
114
+
115
+ # Удаление задач
116
+ for task_id in [conn_id, f"{conn_id}_server"]:
117
+ if task_id in self._tasks:
118
+ self._tasks[task_id].cancel()
119
+ del self._tasks[task_id]
120
+
121
+ writer.close()
122
+ await writer.wait_closed()
123
+
124
+ self.logger.debug(f"Connection closed: {client_ip}")
125
+
126
+ async def _forward(
127
+ self,
128
+ reader: asyncio.StreamReader,
129
+ writer: asyncio.StreamWriter,
130
+ conn_id: str,
131
+ sent: bool = False,
132
+ received: bool = False
133
+ ):
134
+ """Пересылка данных с обновлением статистики"""
135
+ try:
136
+ while True:
137
+ data = await reader.read(8192)
138
+ if not data:
139
+ break
140
+
141
+ writer.write(data)
142
+ await writer.drain()
143
+
144
+ # Обновление статистики
145
+ if sent:
146
+ self.stats.update_bytes(conn_id, sent=len(data))
147
+ if received:
148
+ self.stats.update_bytes(conn_id, received=len(data))
149
+
150
+ except (asyncio.CancelledError, ConnectionError):
151
+ pass
152
+ except Exception as e:
153
+ self.logger.debug(f"Forward error: {e}")
154
+ finally:
155
+ writer.close()
156
+
157
+ async def _stats_reporter(self):
158
+ """Периодический вывод статистики"""
159
+ while self._running:
160
+ await asyncio.sleep(self.config.stats_interval)
161
+ self.logger.info("\n" + self.stats.format_summary())
162
+
163
+ def start(self):
164
+ """Запуск прокси сервера"""
165
+ loop = asyncio.new_event_loop()
166
+ asyncio.set_event_loop(loop)
167
+
168
+ self._running = True
169
+
170
+ try:
171
+ # Запуск сервера
172
+ self.server = loop.run_until_complete(
173
+ asyncio.start_server(
174
+ self._handle_connection,
175
+ '0.0.0.0',
176
+ self.config.port
177
+ )
178
+ )
179
+
180
+ # Запуск репортера статистики
181
+ stats_task = loop.create_task(self._stats_reporter())
182
+
183
+ # Вывод информации
184
+ print("=" * 60)
185
+ print("🚀 MTProxy Server Started")
186
+ print("=" * 60)
187
+ print(f"📡 Port: {self.config.port}")
188
+ print(f"🔐 Fake TLS: {self.config.enable_fake_tls}")
189
+ print(f"🔑 Secrets count: {len(self.config.secrets)}")
190
+ print(f"📊 Stats interval: {self.config.stats_interval}s")
191
+ print("-" * 60)
192
+ print("Connection links:")
193
+
194
+ ip = get_external_ip()
195
+ for secret, tag in self.config.secrets.items():
196
+ link = f"tg://proxy?server={ip}&port={self.config.port}&secret={secret}"
197
+ if tag:
198
+ link += f"&tag={tag}"
199
+ print(f" {link}")
200
+
201
+ print("=" * 60)
202
+ print("Press Ctrl+C to stop")
203
+ print("=" * 60)
204
+
205
+ # Обработка сигналов
206
+ for sig in (signal.SIGINT, signal.SIGTERM):
207
+ loop.add_signal_handler(sig, lambda: asyncio.create_task(self.stop()))
208
+
209
+ loop.run_forever()
210
+
211
+ except KeyboardInterrupt:
212
+ pass
213
+ except Exception as e:
214
+ self.logger.error(f"Server error: {e}")
215
+ finally:
216
+ stats_task.cancel()
217
+ self._running = False
218
+ if self.server:
219
+ self.server.close()
220
+ loop.close()
221
+
222
+ async def stop(self):
223
+ """Остановка сервера"""
224
+ self.logger.info("Shutting down...")
225
+ self._running = False
226
+
227
+ # Отмена всех задач
228
+ for task in self._tasks.values():
229
+ task.cancel()
230
+
231
+ if self.server:
232
+ self.server.close()
233
+ await self.server.wait_closed()
234
+
235
+ self.logger.info("Server stopped")
@@ -0,0 +1,167 @@
1
+ """Модуль статистики"""
2
+
3
+ import time
4
+ import threading
5
+ from dataclasses import dataclass, field
6
+ from typing import Dict, Optional
7
+ from collections import defaultdict
8
+ from datetime import datetime
9
+
10
+
11
+ @dataclass
12
+ class ConnectionStats:
13
+ """Статистика по одному соединению"""
14
+ client_ip: str
15
+ secret_key: str
16
+ start_time: float
17
+ bytes_sent: int = 0
18
+ bytes_received: int = 0
19
+ end_time: Optional[float] = None
20
+
21
+ @property
22
+ def duration(self) -> float:
23
+ """Длительность соединения в секундах"""
24
+ end = self.end_time or time.time()
25
+ return end - self.start_time
26
+
27
+ @property
28
+ def total_bytes(self) -> int:
29
+ """Общее количество байт"""
30
+ return self.bytes_sent + self.bytes_received
31
+
32
+
33
+ class ProxyStats:
34
+ """
35
+ Сбор статистики прокси сервера
36
+
37
+ Attributes:
38
+ total_connections (int): Всего подключений
39
+ active_connections (int): Активных подключений
40
+ total_bytes_sent (int): Всего отправлено байт
41
+ total_bytes_received (int): Всего получено байт
42
+ connections_per_secret (dict): Статистика по секретам
43
+ connections_per_ip (dict): Статистика по IP
44
+ """
45
+
46
+ def __init__(self):
47
+ self._lock = threading.Lock()
48
+ self._connections: Dict[str, ConnectionStats] = {}
49
+ self._start_time = time.time()
50
+
51
+ # Общая статистика
52
+ self.total_connections = 0
53
+ self.active_connections = 0
54
+ self.total_bytes_sent = 0
55
+ self.total_bytes_received = 0
56
+
57
+ # Детальная статистика
58
+ self.connections_per_secret: Dict[str, int] = defaultdict(int)
59
+ self.connections_per_ip: Dict[str, int] = defaultdict(int)
60
+ self.bytes_per_secret: Dict[str, Dict[str, int]] = defaultdict(
61
+ lambda: {'sent': 0, 'received': 0}
62
+ )
63
+
64
+ def connection_started(self, conn_id: str, client_ip: str, secret_key: str) -> None:
65
+ """Начало соединения"""
66
+ with self._lock:
67
+ self._connections[conn_id] = ConnectionStats(
68
+ client_ip=client_ip,
69
+ secret_key=secret_key,
70
+ start_time=time.time()
71
+ )
72
+ self.total_connections += 1
73
+ self.active_connections += 1
74
+ self.connections_per_secret[secret_key] += 1
75
+ self.connections_per_ip[client_ip] += 1
76
+
77
+ def connection_ended(self, conn_id: str) -> Optional[ConnectionStats]:
78
+ """Завершение соединения"""
79
+ with self._lock:
80
+ if conn_id not in self._connections:
81
+ return None
82
+
83
+ conn = self._connections[conn_id]
84
+ conn.end_time = time.time()
85
+
86
+ self.active_connections -= 1
87
+ self.total_bytes_sent += conn.bytes_sent
88
+ self.total_bytes_received += conn.bytes_received
89
+
90
+ # Обновление статистики по секрету
91
+ self.bytes_per_secret[conn.secret_key]['sent'] += conn.bytes_sent
92
+ self.bytes_per_secret[conn.secret_key]['received'] += conn.bytes_received
93
+
94
+ return self._connections.pop(conn_id, None)
95
+
96
+ def update_bytes(self, conn_id: str, sent: int = 0, received: int = 0) -> None:
97
+ """Обновление счетчиков байт"""
98
+ with self._lock:
99
+ if conn_id in self._connections:
100
+ self._connections[conn_id].bytes_sent += sent
101
+ self._connections[conn_id].bytes_received += received
102
+
103
+ def get_summary(self) -> dict:
104
+ """Получение сводной статистики"""
105
+ with self._lock:
106
+ uptime = time.time() - self._start_time
107
+
108
+ return {
109
+ 'uptime_seconds': uptime,
110
+ 'total_connections': self.total_connections,
111
+ 'active_connections': self.active_connections,
112
+ 'total_bytes_sent': self.total_bytes_sent,
113
+ 'total_bytes_received': self.total_bytes_received,
114
+ 'total_bytes': self.total_bytes_sent + self.total_bytes_received,
115
+ 'connections_per_second': self.total_connections / uptime if uptime > 0 else 0,
116
+ 'bytes_per_second': (self.total_bytes_sent + self.total_bytes_received) / uptime if uptime > 0 else 0,
117
+ 'secrets': dict(self.connections_per_secret),
118
+ 'bytes_per_secret': dict(self.bytes_per_secret),
119
+ 'top_ips': sorted(
120
+ self.connections_per_ip.items(),
121
+ key=lambda x: x[1],
122
+ reverse=True
123
+ )[:10]
124
+ }
125
+
126
+ def format_summary(self) -> str:
127
+ """Форматирование сводки для вывода"""
128
+ stats = self.get_summary()
129
+
130
+ def format_bytes(b: int) -> str:
131
+ for unit in ['B', 'KB', 'MB', 'GB']:
132
+ if b < 1024:
133
+ return f"{b:.2f} {unit}"
134
+ b /= 1024
135
+ return f"{b:.2f} TB"
136
+
137
+ lines = [
138
+ "=" * 50,
139
+ "MTProxy Statistics",
140
+ "=" * 50,
141
+ f"Uptime: {stats['uptime_seconds']:.0f} seconds",
142
+ f"Total connections: {stats['total_connections']}",
143
+ f"Active connections: {stats['active_connections']}",
144
+ f"Total traffic: {format_bytes(stats['total_bytes'])}",
145
+ f" - Sent: {format_bytes(stats['total_bytes_sent'])}",
146
+ f" - Received: {format_bytes(stats['total_bytes_received'])}",
147
+ f"Connections/sec: {stats['connections_per_second']:.2f}",
148
+ f"Traffic/sec: {format_bytes(stats['bytes_per_second'])}",
149
+ "-" * 50,
150
+ "Statistics by secret:",
151
+ ]
152
+
153
+ for secret, count in stats['secrets'].items():
154
+ bytes_stats = stats['bytes_per_secret'].get(secret, {'sent': 0, 'received': 0})
155
+ total = bytes_stats['sent'] + bytes_stats['received']
156
+ lines.append(
157
+ f" {secret[:16]}...: {count} connections, {format_bytes(total)}"
158
+ )
159
+
160
+ if stats['top_ips']:
161
+ lines.append("-" * 50)
162
+ lines.append("Top 10 IPs:")
163
+ for ip, count in stats['top_ips']:
164
+ lines.append(f" {ip}: {count} connections")
165
+
166
+ lines.append("=" * 50)
167
+ return "\n".join(lines)
@@ -0,0 +1,58 @@
1
+ Metadata-Version: 2.4
2
+ Name: py-mtproxy-lib
3
+ Version: 0.0.2
4
+ Summary: Production-ready MTProto proxy server library with multi-secret support, statistics, and daemon mode
5
+ Home-page: https://github.com/twosleepynights0x1/py-mtproxy-lib
6
+ Author: Your Name
7
+ Author-email: tashova28@gmail.com
8
+ Project-URL: Bug Reports, https://github.com/twosleepynights0x1/py-mtproxy-lib
9
+ Keywords: mtproto telegram proxy vpn daemon docker
10
+ Classifier: Development Status :: 5 - Production/Stable
11
+ Classifier: Intended Audience :: System Administrators
12
+ Classifier: Topic :: Internet :: Proxy Servers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.7
15
+ Classifier: Programming Language :: Python :: 3.8
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: License :: OSI Approved :: MIT License
20
+ Classifier: Operating System :: POSIX
21
+ Classifier: Operating System :: Unix
22
+ Requires-Python: >=3.7
23
+ Description-Content-Type: text/markdown
24
+ Requires-Dist: pycryptodome>=3.10.0
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=6.0; extra == "dev"
27
+ Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
28
+ Requires-Dist: black>=22.0; extra == "dev"
29
+ Requires-Dist: flake8>=4.0; extra == "dev"
30
+ Provides-Extra: docker
31
+ Requires-Dist: docker>=6.0; extra == "docker"
32
+ Dynamic: author
33
+ Dynamic: author-email
34
+ Dynamic: classifier
35
+ Dynamic: description
36
+ Dynamic: description-content-type
37
+ Dynamic: home-page
38
+ Dynamic: keywords
39
+ Dynamic: project-url
40
+ Dynamic: provides-extra
41
+ Dynamic: requires-dist
42
+ Dynamic: requires-python
43
+ Dynamic: summary
44
+
45
+ cat > README.md << 'EOF'
46
+ # py-mtproxy-lib
47
+
48
+ Python библиотека для запуска MTProto прокси сервера для Telegram.
49
+
50
+ ## Возможности
51
+
52
+ - Поддержка нескольких секретов
53
+ - Fake TLS маскировка
54
+ - Статистика подключений и трафика
55
+ - Логирование с разными уровнями
56
+ - Запуск как системный демон
57
+ - Docker образ
58
+
@@ -0,0 +1,15 @@
1
+ README.md
2
+ setup.py
3
+ mtproxy/__init__.py
4
+ mtproxy/cli.py
5
+ mtproxy/config.py
6
+ mtproxy/daemon.py
7
+ mtproxy/logger.py
8
+ mtproxy/server.py
9
+ mtproxy/stats.py
10
+ py_mtproxy_lib.egg-info/PKG-INFO
11
+ py_mtproxy_lib.egg-info/SOURCES.txt
12
+ py_mtproxy_lib.egg-info/dependency_links.txt
13
+ py_mtproxy_lib.egg-info/entry_points.txt
14
+ py_mtproxy_lib.egg-info/requires.txt
15
+ py_mtproxy_lib.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ mtproxy = mtproxy.cli:main
@@ -0,0 +1,10 @@
1
+ pycryptodome>=3.10.0
2
+
3
+ [dev]
4
+ pytest>=6.0
5
+ pytest-asyncio>=0.21.0
6
+ black>=22.0
7
+ flake8>=4.0
8
+
9
+ [docker]
10
+ docker>=6.0
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,58 @@
1
+ from setuptools import setup, find_packages
2
+
3
+ with open("README.md", "r", encoding="utf-8") as fh:
4
+ long_description = fh.read()
5
+
6
+ setup(
7
+ name="py-mtproxy-lib",
8
+ version="0.0.2",
9
+ author="Your Name",
10
+ author_email="tashova28@gmail.com",
11
+ description="Production-ready MTProto proxy server library with multi-secret support, statistics, and daemon mode",
12
+ long_description=long_description,
13
+ long_description_content_type="text/markdown",
14
+ url="https://github.com/twosleepynights0x1/py-mtproxy-lib",
15
+ packages=find_packages(),
16
+ classifiers=[
17
+ "Development Status :: 5 - Production/Stable",
18
+ "Intended Audience :: System Administrators",
19
+ "Topic :: Internet :: Proxy Servers",
20
+ "Programming Language :: Python :: 3",
21
+ "Programming Language :: Python :: 3.7",
22
+ "Programming Language :: Python :: 3.8",
23
+ "Programming Language :: Python :: 3.9",
24
+ "Programming Language :: Python :: 3.10",
25
+ "Programming Language :: Python :: 3.11",
26
+ "License :: OSI Approved :: MIT License",
27
+ "Operating System :: POSIX",
28
+ "Operating System :: Unix",
29
+ ],
30
+ python_requires=">=3.7",
31
+ install_requires=[
32
+ "pycryptodome>=3.10.0",
33
+ ],
34
+ extras_require={
35
+ "dev": [
36
+ "pytest>=6.0",
37
+ "pytest-asyncio>=0.21.0",
38
+ "black>=22.0",
39
+ "flake8>=4.0",
40
+ ],
41
+ "docker": [
42
+ "docker>=6.0",
43
+ ],
44
+ },
45
+ entry_points={
46
+ "console_scripts": [
47
+ "mtproxy=mtproxy.cli:main",
48
+ ],
49
+ },
50
+ package_data={
51
+ "mtproxy": ["py.typed"],
52
+ },
53
+ include_package_data=True,
54
+ keywords="mtproto telegram proxy vpn daemon docker",
55
+ project_urls={
56
+ "Bug Reports": "https://github.com/twosleepynights0x1/py-mtproxy-lib",
57
+ },
58
+ )