proxynt 2.1.0__tar.gz → 2.1.1__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.
- {proxynt-2.1.0/proxynt.egg-info → proxynt-2.1.1}/PKG-INFO +1 -6
- proxynt-2.1.1/common/captcha.py +123 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/pool.py +22 -12
- {proxynt-2.1.0 → proxynt-2.1.1}/constant/system_constant.py +1 -1
- {proxynt-2.1.0 → proxynt-2.1.1/proxynt.egg-info}/PKG-INFO +1 -6
- {proxynt-2.1.0 → proxynt-2.1.1}/proxynt.egg-info/SOURCES.txt +1 -0
- proxynt-2.1.1/proxynt.egg-info/entry_points.txt +3 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/server/admin_http_handler.py +60 -5
- {proxynt-2.1.0 → proxynt-2.1.1}/server/tcp_forward_client.py +67 -20
- {proxynt-2.1.0 → proxynt-2.1.1}/server/template/login.html +19 -2
- {proxynt-2.1.0 → proxynt-2.1.1}/server/websocket_handler.py +10 -19
- proxynt-2.1.0/proxynt.egg-info/entry_points.txt +0 -5
- {proxynt-2.1.0 → proxynt-2.1.1}/LICENSE +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/MANIFEST.in +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/__init__.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/client/__init__.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/client/abstract_tunnel.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/client/clear_nonce_task.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/client/heart_beat_task.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/client/kcp_tunnel_impl.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/client/n4_tunnel_manager.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/client/quic_tunnel_impl.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/client/tcp_forward_client.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/client/tunnel_protocol.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/client/udp_forward_client.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/__init__.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/cert_utils.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/crypto/__init__.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/crypto/table.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/encrypt_utils.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/kcp.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/logger_factory.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/n4_protocol.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/n4_punch.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/nat_serialization.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/nat_serialization_v1.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/nat_serialization_v2.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/register_append_data.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/speed_limit.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/__init__.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/_abnf.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/_app.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/_cookiejar.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/_core.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/_dispatcher.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/_exceptions.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/_handshake.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/_http.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/_logging.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/_socket.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/_ssl_compat.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/_url.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/_utils.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/_wsdump.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/tests/__init__.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/tests/echo-server.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/tests/test_abnf.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/tests/test_app.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/tests/test_cookiejar.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/tests/test_dispatcher.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/tests/test_handshake_large_response.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/tests/test_http.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/tests/test_large_payloads.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/tests/test_logging_helpers.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/tests/test_reconnect_bad_fd.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/tests/test_socket.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/tests/test_socket_bugs.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/tests/test_ssl_compat.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/tests/test_ssl_edge_cases.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/tests/test_url.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/tests/test_utils.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket/tests/test_websocket.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/__init__.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/compliance/autobahn-test-report-Feb-03-2021.html +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/setup.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/__init__.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/_abnf.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/_app.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/_cookiejar.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/_core.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/_dispatcher.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/_exceptions.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/_handshake.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/_http.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/_logging.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/_socket.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/_ssl_compat.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/_url.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/_utils.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/_wsdump.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/__init__.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/echo-server.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/test_abnf.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/test_app.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/test_cookiejar.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/test_dispatcher.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/test_handshake_large_response.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/test_http.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/test_large_payloads.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/test_logging_helpers.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/test_reconnect_bad_fd.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/test_socket.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/test_socket_bugs.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/test_ssl_compat.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/test_ssl_edge_cases.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/test_url.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/test_utils.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/test_websocket.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/constant/__init__.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/constant/message_type_constnat.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/context/__init__.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/context/context_utils.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/entity/__init__.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/entity/client_config_entity.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/entity/message/__init__.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/entity/message/message_entity.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/entity/message/push_config_entity.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/entity/message/tcp_over_websocket_message.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/entity/server_config_entity.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/exceptions/__init__.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/exceptions/duplicated_name.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/exceptions/invalid_password.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/exceptions/replay_error.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/exceptions/signature_error.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/p2ptest/__init__.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/p2ptest/client.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/p2ptest/n4.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/p2ptest/tls_handshake_timeout_test.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/proxynt.egg-info/dependency_links.txt +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/proxynt.egg-info/requires.txt +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/proxynt.egg-info/top_level.txt +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/run_client.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/run_server.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/server/__init__.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/server/n4.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/server/n4_signal_service.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/server/task/__init__.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/server/task/check_cookie_task.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/server/task/clear_nonce_task.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/server/task/heart_beat_task.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/server/template/__init__.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/server/template/base.html +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/server/template/css/fonts/element-icons.woff +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/server/template/css/index.css +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/server/template/ele_index.html +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/server/template/js/axios.min.js +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/server/template/js/index.js +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/server/template/js/vue.min.js +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/server/udp_forward_client.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/setup.cfg +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/setup.py +0 -0
- {proxynt-2.1.0 → proxynt-2.1.1}/test_exchange.py +0 -0
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: proxynt
|
|
3
|
-
Version: 2.1.
|
|
4
|
-
Summary: UNKNOWN
|
|
3
|
+
Version: 2.1.1
|
|
5
4
|
Home-page: https://github.com/sazima/proxynt
|
|
6
|
-
License: UNKNOWN
|
|
7
|
-
Platform: UNKNOWN
|
|
8
5
|
Description-Content-Type: text/markdown
|
|
9
6
|
Provides-Extra: snappy
|
|
10
7
|
License-File: LICENSE
|
|
@@ -175,5 +172,3 @@ ssh -oPort=12222 test@192.168.9.224
|
|
|
175
172
|
|
|
176
173
|
|
|
177
174
|
|
|
178
|
-
|
|
179
|
-
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import random
|
|
2
|
+
import secrets
|
|
3
|
+
import time
|
|
4
|
+
from typing import Dict, Tuple
|
|
5
|
+
|
|
6
|
+
"""
|
|
7
|
+
登录验证码 (纯标准库生成 SVG) 与按 IP 的登录失败锁定
|
|
8
|
+
只在 tornado 线程中使用, 不加锁
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
CAPTCHA_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789' # 去掉 0/O、1/I/L 等易混淆字符
|
|
12
|
+
CAPTCHA_LENGTH = 4
|
|
13
|
+
CAPTCHA_WIDTH = 120
|
|
14
|
+
CAPTCHA_HEIGHT = 44
|
|
15
|
+
CAPTCHA_EXPIRE_SECONDS = 120
|
|
16
|
+
CAPTCHA_MAX_STORE = 1000
|
|
17
|
+
|
|
18
|
+
LOGIN_MAX_FAILURES = 5 # 同一 IP 连续失败次数上限
|
|
19
|
+
LOGIN_LOCK_SECONDS = 300
|
|
20
|
+
LOGIN_MAX_TRACKED_IPS = 10000
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _rand_color(low: int, high: int) -> str:
|
|
24
|
+
return '#%02x%02x%02x' % (random.randint(low, high), random.randint(low, high), random.randint(low, high))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _render_svg(code: str) -> str:
|
|
28
|
+
parts = [f'<svg xmlns="http://www.w3.org/2000/svg" width="{CAPTCHA_WIDTH}" height="{CAPTCHA_HEIGHT}" '
|
|
29
|
+
f'viewBox="0 0 {CAPTCHA_WIDTH} {CAPTCHA_HEIGHT}">',
|
|
30
|
+
f'<rect width="100%" height="100%" fill="{_rand_color(225, 250)}"/>']
|
|
31
|
+
# 噪点
|
|
32
|
+
for _ in range(30):
|
|
33
|
+
parts.append(f'<circle cx="{random.uniform(0, CAPTCHA_WIDTH):.1f}" cy="{random.uniform(0, CAPTCHA_HEIGHT):.1f}" '
|
|
34
|
+
f'r="{random.uniform(0.5, 1.5):.1f}" fill="{_rand_color(100, 200)}"/>')
|
|
35
|
+
# 字符: 随机字号、旋转、偏移
|
|
36
|
+
step = CAPTCHA_WIDTH / (CAPTCHA_LENGTH + 1)
|
|
37
|
+
for i, ch in enumerate(code):
|
|
38
|
+
x = step * (i + 0.6) + random.uniform(-4, 4)
|
|
39
|
+
y = CAPTCHA_HEIGHT * 0.68 + random.uniform(-3, 3)
|
|
40
|
+
parts.append(f'<text x="{x:.1f}" y="{y:.1f}" font-size="{random.randint(22, 26)}" font-family="Arial,sans-serif" '
|
|
41
|
+
f'font-weight="bold" fill="{_rand_color(20, 120)}" '
|
|
42
|
+
f'transform="rotate({random.randint(-25, 25)} {x:.1f} {y:.1f})">{ch}</text>')
|
|
43
|
+
# 干扰曲线
|
|
44
|
+
for _ in range(3):
|
|
45
|
+
parts.append(f'<path d="M{random.uniform(0, 20):.1f} {random.uniform(0, CAPTCHA_HEIGHT):.1f} '
|
|
46
|
+
f'Q{random.uniform(30, 90):.1f} {random.uniform(-10, CAPTCHA_HEIGHT + 10):.1f} '
|
|
47
|
+
f'{random.uniform(100, CAPTCHA_WIDTH):.1f} {random.uniform(0, CAPTCHA_HEIGHT):.1f}" '
|
|
48
|
+
f'stroke="{_rand_color(60, 160)}" stroke-width="{random.uniform(1, 2):.1f}" fill="none"/>')
|
|
49
|
+
parts.append('</svg>')
|
|
50
|
+
return ''.join(parts)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class CaptchaStore:
|
|
54
|
+
"""captcha_id -> (答案, 生成时间), 校验一次即作废"""
|
|
55
|
+
|
|
56
|
+
def __init__(self):
|
|
57
|
+
self._store: Dict[str, Tuple[str, float]] = dict()
|
|
58
|
+
|
|
59
|
+
def create(self) -> Tuple[str, str]:
|
|
60
|
+
"""返回 (captcha_id, svg)"""
|
|
61
|
+
if len(self._store) >= CAPTCHA_MAX_STORE:
|
|
62
|
+
self._clean()
|
|
63
|
+
if len(self._store) >= CAPTCHA_MAX_STORE:
|
|
64
|
+
self._store.pop(next(iter(self._store))) # 仍然满则淘汰最早的
|
|
65
|
+
code = ''.join(secrets.choice(CAPTCHA_CHARS) for _ in range(CAPTCHA_LENGTH))
|
|
66
|
+
captcha_id = secrets.token_urlsafe(16)
|
|
67
|
+
self._store[captcha_id] = (code, time.time())
|
|
68
|
+
return captcha_id, _render_svg(code)
|
|
69
|
+
|
|
70
|
+
def verify(self, captcha_id: str, answer: str) -> bool:
|
|
71
|
+
if not captcha_id or not answer:
|
|
72
|
+
return False
|
|
73
|
+
item = self._store.pop(captcha_id, None)
|
|
74
|
+
if item is None:
|
|
75
|
+
return False
|
|
76
|
+
code, create_time = item
|
|
77
|
+
if time.time() - create_time > CAPTCHA_EXPIRE_SECONDS:
|
|
78
|
+
return False
|
|
79
|
+
return secrets.compare_digest(code, answer.strip().upper())
|
|
80
|
+
|
|
81
|
+
def _clean(self):
|
|
82
|
+
now = time.time()
|
|
83
|
+
for k in [k for k, (_, t) in self._store.items() if now - t > CAPTCHA_EXPIRE_SECONDS]:
|
|
84
|
+
self._store.pop(k, None)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class LoginLimiter:
|
|
88
|
+
"""按 IP 记录连续失败次数, 超过上限锁定一段时间"""
|
|
89
|
+
|
|
90
|
+
def __init__(self):
|
|
91
|
+
self._failures: Dict[str, Tuple[int, float]] = dict() # ip -> (连续失败次数, 最后失败时间)
|
|
92
|
+
|
|
93
|
+
def locked_seconds(self, ip: str) -> int:
|
|
94
|
+
"""剩余锁定秒数, 0 表示未锁定"""
|
|
95
|
+
item = self._failures.get(ip)
|
|
96
|
+
if not item:
|
|
97
|
+
return 0
|
|
98
|
+
count, last_time = item
|
|
99
|
+
remain = LOGIN_LOCK_SECONDS - (time.time() - last_time)
|
|
100
|
+
if remain <= 0:
|
|
101
|
+
if count >= LOGIN_MAX_FAILURES:
|
|
102
|
+
self._failures.pop(ip, None) # 锁定结束, 重新计数
|
|
103
|
+
return 0
|
|
104
|
+
return int(remain) + 1 if count >= LOGIN_MAX_FAILURES else 0
|
|
105
|
+
|
|
106
|
+
def record_failure(self, ip: str) -> int:
|
|
107
|
+
"""记录一次失败, 返回剩余可尝试次数"""
|
|
108
|
+
if len(self._failures) >= LOGIN_MAX_TRACKED_IPS:
|
|
109
|
+
self._clean()
|
|
110
|
+
count, last_time = self._failures.get(ip, (0, 0))
|
|
111
|
+
if time.time() - last_time > LOGIN_LOCK_SECONDS:
|
|
112
|
+
count = 0
|
|
113
|
+
count += 1
|
|
114
|
+
self._failures[ip] = (count, time.time())
|
|
115
|
+
return max(LOGIN_MAX_FAILURES - count, 0)
|
|
116
|
+
|
|
117
|
+
def record_success(self, ip: str):
|
|
118
|
+
self._failures.pop(ip, None)
|
|
119
|
+
|
|
120
|
+
def _clean(self):
|
|
121
|
+
now = time.time()
|
|
122
|
+
for k in [k for k, (_, t) in self._failures.items() if now - t > LOGIN_LOCK_SECONDS]:
|
|
123
|
+
self._failures.pop(k, None)
|
|
@@ -17,6 +17,8 @@ from constant.system_constant import SystemConstant
|
|
|
17
17
|
|
|
18
18
|
PAUSE_REASON_SPEED_LIMIT = 'speed_limit'
|
|
19
19
|
PAUSE_REASON_FLOW_CONTROL = 'flow_control'
|
|
20
|
+
PAUSE_REASON_EARLY_DATA = 'early_data'
|
|
21
|
+
PAUSE_REASON_WS_BACKPRESSURE = 'ws_backpressure'
|
|
20
22
|
|
|
21
23
|
|
|
22
24
|
class SelectPool:
|
|
@@ -32,6 +34,7 @@ class SelectPool:
|
|
|
32
34
|
self.selector.register(self._wakeup_r, EVENT_READ, None)
|
|
33
35
|
# 每个 socket 的暂停原因 (如 speed_limit / flow_control), 全部解除才恢复读取
|
|
34
36
|
self._pause_reasons: Dict[socket.socket, Set[str]] = dict()
|
|
37
|
+
self._paused_data: Dict[socket.socket, ResisterAppendData] = dict() # 暂停期间 socket 不在 selector 中, 保存其 data
|
|
35
38
|
self._pause_lock = threading.Lock()
|
|
36
39
|
|
|
37
40
|
def _wakeup(self):
|
|
@@ -68,6 +71,7 @@ class SelectPool:
|
|
|
68
71
|
self.fileno_to_client.pop(fileno)
|
|
69
72
|
with self._pause_lock:
|
|
70
73
|
self._pause_reasons.pop(s, None)
|
|
74
|
+
self._paused_data.pop(s, None)
|
|
71
75
|
try:
|
|
72
76
|
self.selector.unregister(s)
|
|
73
77
|
except (KeyError, ValueError):
|
|
@@ -76,20 +80,26 @@ class SelectPool:
|
|
|
76
80
|
LoggerFactory.get_logger().error(traceback.format_exc())
|
|
77
81
|
|
|
78
82
|
def pause_reading(self, s: socket.socket, reason: str = PAUSE_REASON_SPEED_LIMIT):
|
|
79
|
-
"""
|
|
83
|
+
"""
|
|
84
|
+
暂停监听可读事件
|
|
85
|
+
不能用 modify(s, 0): Python 3.6 的 modify 会先 unregister 再以 0 掩码 register 抛 ValueError, socket 被永久移除
|
|
86
|
+
这里直接 unregister 并保存 data, 恢复时重新 register
|
|
87
|
+
"""
|
|
80
88
|
with self._pause_lock:
|
|
89
|
+
reasons = self._pause_reasons.get(s)
|
|
90
|
+
if reasons:
|
|
91
|
+
reasons.add(reason)
|
|
92
|
+
return
|
|
81
93
|
try:
|
|
82
94
|
key = self.selector.get_key(s)
|
|
83
95
|
except (KeyError, ValueError):
|
|
84
96
|
return
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
except (KeyError, ValueError):
|
|
92
|
-
pass
|
|
97
|
+
self._pause_reasons[s] = {reason}
|
|
98
|
+
self._paused_data[s] = key.data
|
|
99
|
+
try:
|
|
100
|
+
self.selector.unregister(s)
|
|
101
|
+
except (KeyError, ValueError):
|
|
102
|
+
pass
|
|
93
103
|
|
|
94
104
|
def resume_reading(self, s: socket.socket, reason: str = PAUSE_REASON_SPEED_LIMIT):
|
|
95
105
|
"""解除一个暂停原因, 没有其他原因时恢复监听可读事件"""
|
|
@@ -101,10 +111,10 @@ class SelectPool:
|
|
|
101
111
|
if reasons:
|
|
102
112
|
return
|
|
103
113
|
self._pause_reasons.pop(s, None)
|
|
114
|
+
data = self._paused_data.pop(s, None)
|
|
104
115
|
try:
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
except (KeyError, ValueError):
|
|
116
|
+
self.selector.register(s, EVENT_READ, data)
|
|
117
|
+
except (KeyError, ValueError, OSError):
|
|
108
118
|
return
|
|
109
119
|
self._wakeup()
|
|
110
120
|
|
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: proxynt
|
|
3
|
-
Version: 2.1.
|
|
4
|
-
Summary: UNKNOWN
|
|
3
|
+
Version: 2.1.1
|
|
5
4
|
Home-page: https://github.com/sazima/proxynt
|
|
6
|
-
License: UNKNOWN
|
|
7
|
-
Platform: UNKNOWN
|
|
8
5
|
Description-Content-Type: text/markdown
|
|
9
6
|
Provides-Extra: snappy
|
|
10
7
|
License-File: LICENSE
|
|
@@ -175,5 +172,3 @@ ssh -oPort=12222 test@192.168.9.224
|
|
|
175
172
|
|
|
176
173
|
|
|
177
174
|
|
|
178
|
-
|
|
179
|
-
|
|
@@ -7,6 +7,7 @@ from typing import List, Set, Tuple
|
|
|
7
7
|
|
|
8
8
|
from tornado.web import RequestHandler
|
|
9
9
|
|
|
10
|
+
from common.captcha import CaptchaStore, LoginLimiter
|
|
10
11
|
from common.logger_factory import LoggerFactory
|
|
11
12
|
from constant.system_constant import SystemConstant
|
|
12
13
|
from context.context_utils import ContextUtils
|
|
@@ -17,25 +18,69 @@ from server.websocket_handler import MyWebSocketaHandler
|
|
|
17
18
|
|
|
18
19
|
# todo: 身份认证
|
|
19
20
|
COOKIE_KEY = 'c'
|
|
21
|
+
CAPTCHA_COOKIE_KEY = 'cid'
|
|
20
22
|
MIN_PORT = 1000
|
|
21
23
|
NOT_LOGIN = 401
|
|
22
24
|
|
|
25
|
+
captcha_store = CaptchaStore()
|
|
26
|
+
login_limiter = LoginLimiter()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_login(handler: RequestHandler) -> bool:
|
|
30
|
+
cookie_dict = ContextUtils.get_cookie_to_time()
|
|
31
|
+
result = handler.get_cookie(COOKIE_KEY)
|
|
32
|
+
return result in cookie_dict and time.time() - cookie_dict[result] < SystemConstant.COOKIE_EXPIRE_SECONDS
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_client_ip(handler: RequestHandler) -> str:
|
|
36
|
+
"""只有经本机反向代理 (nginx) 转发时才信任 X-Real-IP, 防止直连时伪造请求头绕过失败锁定"""
|
|
37
|
+
remote_ip = handler.request.remote_ip
|
|
38
|
+
if remote_ip in ('127.0.0.1', '::1'):
|
|
39
|
+
return handler.request.headers.get('X-Real-IP') or remote_ip
|
|
40
|
+
return remote_ip
|
|
41
|
+
|
|
23
42
|
|
|
24
43
|
class AdminHtmlHandler(RequestHandler):
|
|
25
44
|
async def get(self):
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
45
|
+
if self.get_argument('captcha', ''):
|
|
46
|
+
captcha_id, svg = captcha_store.create()
|
|
47
|
+
self.set_cookie(CAPTCHA_COOKIE_KEY, captcha_id, httponly=True)
|
|
48
|
+
self.set_header('Content-Type', 'image/svg+xml')
|
|
49
|
+
self.set_header('Cache-Control', 'no-store')
|
|
50
|
+
self.write(svg)
|
|
51
|
+
return
|
|
52
|
+
if is_login(self):
|
|
29
53
|
self.render('ele_index.html')
|
|
30
54
|
else:
|
|
31
55
|
self.render('login.html')
|
|
32
56
|
|
|
33
57
|
async def post(self):
|
|
34
58
|
try:
|
|
59
|
+
ip = get_client_ip(self)
|
|
60
|
+
locked_seconds = login_limiter.locked_seconds(ip)
|
|
61
|
+
if locked_seconds:
|
|
62
|
+
self.write({
|
|
63
|
+
'code': 400,
|
|
64
|
+
'data': '',
|
|
65
|
+
'msg': f'失败次数过多, 请 {locked_seconds} 秒后再试'
|
|
66
|
+
})
|
|
67
|
+
return
|
|
35
68
|
body_data = json.loads(self.request.body)
|
|
36
|
-
password = body_data
|
|
69
|
+
password = body_data.get('password', '')
|
|
70
|
+
captcha = body_data.get('captcha', '')
|
|
71
|
+
# 不管对错, 验证码只能用一次
|
|
72
|
+
captcha_ok = captcha_store.verify(self.get_cookie(CAPTCHA_COOKIE_KEY), captcha)
|
|
73
|
+
self.clear_cookie(CAPTCHA_COOKIE_KEY)
|
|
74
|
+
if not captcha_ok:
|
|
75
|
+
self.write({
|
|
76
|
+
'code': 400,
|
|
77
|
+
'data': '',
|
|
78
|
+
'msg': '验证码错误或已过期'
|
|
79
|
+
})
|
|
80
|
+
return
|
|
37
81
|
admin_config = ContextUtils.get_admin_config()
|
|
38
82
|
if admin_config and admin_config['admin_password'] == password:
|
|
83
|
+
login_limiter.record_success(ip)
|
|
39
84
|
cookie_value = base64.b64encode(os.urandom(64)).decode()
|
|
40
85
|
cookie_dict = ContextUtils.get_cookie_to_time()
|
|
41
86
|
cookie_dict[cookie_value] = time.time()
|
|
@@ -46,10 +91,12 @@ class AdminHtmlHandler(RequestHandler):
|
|
|
46
91
|
'msg': ''
|
|
47
92
|
})
|
|
48
93
|
else:
|
|
94
|
+
remain = login_limiter.record_failure(ip)
|
|
95
|
+
LoggerFactory.get_logger().warning(f'admin login failed from {ip}, remain attempts: {remain}')
|
|
49
96
|
self.write({
|
|
50
97
|
'code': 400,
|
|
51
98
|
'data': '',
|
|
52
|
-
'msg': '
|
|
99
|
+
'msg': f'密码错误, 还可尝试 {remain} 次' if remain else '密码错误次数过多, 请稍后再试'
|
|
53
100
|
})
|
|
54
101
|
except Exception:
|
|
55
102
|
LoggerFactory.get_logger().error(traceback.format_exc())
|
|
@@ -57,6 +104,14 @@ class AdminHtmlHandler(RequestHandler):
|
|
|
57
104
|
|
|
58
105
|
class ShowVariableHandler(RequestHandler):
|
|
59
106
|
def get(self):
|
|
107
|
+
if not is_login(self):
|
|
108
|
+
self.set_status(NOT_LOGIN)
|
|
109
|
+
self.write({
|
|
110
|
+
'code': NOT_LOGIN,
|
|
111
|
+
'data': '',
|
|
112
|
+
'msg': '未登录'
|
|
113
|
+
})
|
|
114
|
+
return
|
|
60
115
|
forward_client = TcpForwardClient.get_instance()
|
|
61
116
|
dict_ = forward_client.__dict__
|
|
62
117
|
self.write({
|
|
@@ -16,7 +16,7 @@ import tornado
|
|
|
16
16
|
|
|
17
17
|
from common.logger_factory import LoggerFactory
|
|
18
18
|
from common.nat_serialization import NatSerialization
|
|
19
|
-
from common.pool import SelectPool, PAUSE_REASON_FLOW_CONTROL
|
|
19
|
+
from common.pool import SelectPool, PAUSE_REASON_FLOW_CONTROL, PAUSE_REASON_EARLY_DATA, PAUSE_REASON_WS_BACKPRESSURE
|
|
20
20
|
from common.register_append_data import ResisterAppendData
|
|
21
21
|
from common.speed_limit import SpeedLimiter
|
|
22
22
|
from constant.message_type_constnat import MessageTypeConstant
|
|
@@ -59,7 +59,8 @@ class PublicSocketConnection:
|
|
|
59
59
|
self.connection_confirmed = False # Whether connection is confirmed
|
|
60
60
|
self.early_data_buffer = [] # Buffer early data (data received before connection confirmation)
|
|
61
61
|
self.early_data_lock = Lock() # socket 线程缓冲与 tornado 线程 flush 互斥, 防止数据乱序/丢失
|
|
62
|
-
self.
|
|
62
|
+
self.early_data_bytes = 0 # early_data_buffer 中的字节数
|
|
63
|
+
self.early_data_flushed = False # early data 是否已全部写入 websocket, 之前 socket 线程读到的数据都进缓冲, 保证顺序
|
|
63
64
|
self.bytes_from_public: int = 0
|
|
64
65
|
self.bytes_to_public: int = 0
|
|
65
66
|
self.create_time: float = time.time()
|
|
@@ -72,6 +73,7 @@ class PublicSocketConnection:
|
|
|
72
73
|
return f'{self.uid}_{self.socket}_{self.socket_server.name}'
|
|
73
74
|
|
|
74
75
|
|
|
76
|
+
EARLY_DATA_MAX_BYTES = 1 * 1024 * 1024 # 连接确认前最多缓冲的字节数, 超过后暂停读取该公网连接, 确认并 flush 完成后恢复
|
|
75
77
|
SEND_QUEUE_HIGH_WATER = 4 * 1024 * 1024 # 单个 uid 积压超过该值时对来源 websocket 做背压
|
|
76
78
|
SEND_QUEUE_LOW_WATER = 1 * 1024 * 1024
|
|
77
79
|
# 客户端支持流控时: 超过 FLOW_HIGH_WATER 通知客户端暂停该 uid, 低于 FLOW_LOW_WATER 恢复
|
|
@@ -218,12 +220,24 @@ class TcpForwardClient:
|
|
|
218
220
|
LoggerFactory.get_logger().info(
|
|
219
221
|
f'public side closed uid: {socket_connection.uid.hex()}, error: {recv_error}, {socket_connection.stats()}')
|
|
220
222
|
|
|
221
|
-
# Optimistic send mode:
|
|
222
|
-
if socket_connection.optimistic_mode and not socket_connection.
|
|
223
|
+
# Optimistic send mode: early data 未全部 flush 前, 数据都进缓冲
|
|
224
|
+
if socket_connection.optimistic_mode and not socket_connection.early_data_flushed:
|
|
223
225
|
with socket_connection.early_data_lock:
|
|
224
226
|
# 加锁后再判断一次, 避免与 CONNECT_CONFIRMED 的 flush 竞争
|
|
225
|
-
if not socket_connection.
|
|
226
|
-
if
|
|
227
|
+
if not socket_connection.early_data_flushed:
|
|
228
|
+
if recv:
|
|
229
|
+
socket_connection.early_data_buffer.append(recv)
|
|
230
|
+
socket_connection.early_data_bytes += len(recv)
|
|
231
|
+
if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
|
|
232
|
+
LoggerFactory.get_logger().debug(f'Buffering early data uid: {socket_connection.uid}, len: {len(recv)}, buffer_bytes: {socket_connection.early_data_bytes}')
|
|
233
|
+
if socket_connection.early_data_bytes >= EARLY_DATA_MAX_BYTES:
|
|
234
|
+
# 不再读取, 数据留在内核缓冲区, flush 完成后恢复
|
|
235
|
+
self.socket_event_loop.pause_reading(each, PAUSE_REASON_EARLY_DATA)
|
|
236
|
+
return
|
|
237
|
+
# 公网侧已关闭, 剩余 early data 没有接收方了, 直接丢弃
|
|
238
|
+
socket_connection.early_data_buffer = []
|
|
239
|
+
socket_connection.early_data_bytes = 0
|
|
240
|
+
if not socket_connection.connection_confirmed:
|
|
227
241
|
# Client closed connection before confirmation, close immediately
|
|
228
242
|
LoggerFactory.get_logger().info(f'recv empty before confirmed, close uid: {socket_connection.uid}')
|
|
229
243
|
try:
|
|
@@ -231,18 +245,7 @@ class TcpForwardClient:
|
|
|
231
245
|
except (OSError, ValueError, KeyError):
|
|
232
246
|
LoggerFactory.get_logger().error(f'Close error: {traceback.format_exc()}')
|
|
233
247
|
return
|
|
234
|
-
|
|
235
|
-
socket_connection.early_data_buffer.append(recv)
|
|
236
|
-
if LoggerFactory.get_logger().isEnabledFor(logging.DEBUG):
|
|
237
|
-
LoggerFactory.get_logger().debug(f'Buffering early data uid: {socket_connection.uid}, len: {len(recv)}, buffer_size: {len(socket_connection.early_data_buffer)}')
|
|
238
|
-
else:
|
|
239
|
-
# Too much buffered data, connection may have failed, close connection
|
|
240
|
-
LoggerFactory.get_logger().warning(f'Too much early data buffered, closing connection uid: {socket_connection.uid}')
|
|
241
|
-
try:
|
|
242
|
-
self.close_connection(socket_connection)
|
|
243
|
-
except (OSError, ValueError, KeyError):
|
|
244
|
-
LoggerFactory.get_logger().error(f'Close error: {traceback.format_exc()}')
|
|
245
|
-
return
|
|
248
|
+
# 已确认但 flush 未完成: 走下面的正常关闭流程通知客户端
|
|
246
249
|
|
|
247
250
|
# --- 发送端限速:暂停读取,延迟恢复 ---
|
|
248
251
|
if data.speed_limiter and recv:
|
|
@@ -271,11 +274,22 @@ class TcpForwardClient:
|
|
|
271
274
|
handler = socket_connection.socket_server.websocket_handler
|
|
272
275
|
is_compress = handler.compress_support
|
|
273
276
|
protocol_version = handler.protocol_version
|
|
274
|
-
|
|
275
|
-
|
|
277
|
+
message = bytes(NatSerialization.dumps(send_message, ContextUtils.get_password(), is_compress, protocol_version))
|
|
278
|
+
self.tornado_loop.add_callback(self._write_public_data, socket_connection, handler, message)
|
|
276
279
|
except Exception:
|
|
277
280
|
LoggerFactory.get_logger().error(traceback.format_exc())
|
|
278
281
|
|
|
282
|
+
def _write_public_data(self, connection: PublicSocketConnection, handler: 'MyWebSocketaHandler', message: bytes):
|
|
283
|
+
"""
|
|
284
|
+
tornado 线程中把公网数据写入 websocket
|
|
285
|
+
写缓冲积压超过高水位时暂停读取该公网连接, 本次写入 flush 后恢复, 避免大流量占满到客户端的 websocket, 阻塞同一客户端的其他连接
|
|
286
|
+
"""
|
|
287
|
+
future = handler.write_bytes(message)
|
|
288
|
+
if future is None or self.uid_to_connection.get(connection.uid) is not connection:
|
|
289
|
+
return
|
|
290
|
+
self.socket_event_loop.pause_reading(connection.socket, PAUSE_REASON_WS_BACKPRESSURE)
|
|
291
|
+
future.add_done_callback(lambda _: self.socket_event_loop.resume_reading(connection.socket, PAUSE_REASON_WS_BACKPRESSURE))
|
|
292
|
+
|
|
279
293
|
def start_accept(self, server_socket: socket, register_append_data: ResisterAppendData):
|
|
280
294
|
try:
|
|
281
295
|
client, address = server_socket.accept()
|
|
@@ -375,6 +389,39 @@ class TcpForwardClient:
|
|
|
375
389
|
if self.uid_to_send_queue.get(uid) is send_queue:
|
|
376
390
|
self.uid_to_send_queue.pop(uid, None)
|
|
377
391
|
|
|
392
|
+
async def flush_early_data(self, connection: PublicSocketConnection):
|
|
393
|
+
"""
|
|
394
|
+
连接确认后分批把 early data 写入 websocket, 每批做背压
|
|
395
|
+
flush 期间公网连接暂停读取, socket 线程已读到的数据继续进缓冲, 全部写完后才切换为直接转发, 保证顺序
|
|
396
|
+
"""
|
|
397
|
+
handler = connection.socket_server.websocket_handler
|
|
398
|
+
self.socket_event_loop.pause_reading(connection.socket, PAUSE_REASON_EARLY_DATA)
|
|
399
|
+
try:
|
|
400
|
+
while True:
|
|
401
|
+
with connection.early_data_lock:
|
|
402
|
+
batch = connection.early_data_buffer
|
|
403
|
+
connection.early_data_buffer = []
|
|
404
|
+
connection.early_data_bytes = 0
|
|
405
|
+
if not batch:
|
|
406
|
+
connection.early_data_flushed = True
|
|
407
|
+
return
|
|
408
|
+
for buffered_data in batch:
|
|
409
|
+
if self.uid_to_connection.get(connection.uid) is not connection:
|
|
410
|
+
LoggerFactory.get_logger().info(f'connection {connection.uid.hex()} closed, drop remaining early data')
|
|
411
|
+
return
|
|
412
|
+
send_message: MessageEntity = {
|
|
413
|
+
'type_': MessageTypeConstant.WEBSOCKET_OVER_TCP,
|
|
414
|
+
'data': {
|
|
415
|
+
'name': connection.socket_server.name,
|
|
416
|
+
'data': buffered_data,
|
|
417
|
+
'uid': connection.uid,
|
|
418
|
+
'ip_port': connection.socket_server.ip_port
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
await handler.wait_backpressure(handler.forward_message(send_message), handler.client_name)
|
|
422
|
+
finally:
|
|
423
|
+
self.socket_event_loop.resume_reading(connection.socket, PAUSE_REASON_EARLY_DATA)
|
|
424
|
+
|
|
378
425
|
def set_public_reading_paused(self, connection: PublicSocketConnection, pause: bool):
|
|
379
426
|
"""客户端通知暂停/恢复读取公网连接"""
|
|
380
427
|
if pause:
|
|
@@ -9,6 +9,13 @@
|
|
|
9
9
|
<el-form-item label="密码 " :label-width="formLabelWidth">
|
|
10
10
|
<el-input v-model="form.password" autocomplete="off" @keyup.enter.native="handleLogin" type="password"></el-input>
|
|
11
11
|
</el-form-item>
|
|
12
|
+
<el-form-item label="验证码 " :label-width="formLabelWidth">
|
|
13
|
+
<div style="display: flex; align-items: center;">
|
|
14
|
+
<el-input v-model="form.captcha" autocomplete="off" @keyup.enter.native="handleLogin" maxlength="4" style="width: 140px;"></el-input>
|
|
15
|
+
<img :src="captchaUrl" @click="refreshCaptcha" title="看不清? 点击刷新" alt="验证码"
|
|
16
|
+
style="height: 40px; margin-left: 10px; cursor: pointer; border-radius: 4px;">
|
|
17
|
+
</div>
|
|
18
|
+
</el-form-item>
|
|
12
19
|
</el-form>
|
|
13
20
|
</el-col>
|
|
14
21
|
<el-col :span="12">
|
|
@@ -25,23 +32,33 @@
|
|
|
25
32
|
return {
|
|
26
33
|
formLabelWidth: '120px',
|
|
27
34
|
form: {
|
|
28
|
-
password: ''
|
|
29
|
-
|
|
35
|
+
password: '',
|
|
36
|
+
captcha: ''
|
|
37
|
+
},
|
|
38
|
+
captchaUrl: ''
|
|
30
39
|
}
|
|
31
40
|
},
|
|
32
41
|
mounted() {
|
|
42
|
+
this.refreshCaptcha()
|
|
33
43
|
},
|
|
34
44
|
methods: {
|
|
45
|
+
refreshCaptcha() {
|
|
46
|
+
this.form.captcha = ''
|
|
47
|
+
this.captchaUrl = window.location.pathname + '?captcha=1&t=' + Date.now()
|
|
48
|
+
},
|
|
35
49
|
handleLogin() {
|
|
36
50
|
const url = window.location.href
|
|
37
51
|
|
|
38
52
|
axios.post(url, this.form).then(res=> {
|
|
39
53
|
if (res.data.code !== 200) {
|
|
54
|
+
// 验证码只能用一次, 失败后换新的
|
|
55
|
+
this.refreshCaptcha()
|
|
40
56
|
return alert(res.data.msg)
|
|
41
57
|
}
|
|
42
58
|
location.reload();
|
|
43
59
|
}).catch(err => {
|
|
44
60
|
console.log(err)
|
|
61
|
+
this.refreshCaptcha()
|
|
45
62
|
alert('服务器出现错误')
|
|
46
63
|
})
|
|
47
64
|
|
|
@@ -30,7 +30,7 @@ from exceptions.replay_error import ReplayError
|
|
|
30
30
|
from exceptions.signature_error import SignatureError
|
|
31
31
|
from server.tcp_forward_client import TcpForwardClient
|
|
32
32
|
|
|
33
|
-
WRITE_BUFFER_HIGH_WATER =
|
|
33
|
+
WRITE_BUFFER_HIGH_WATER = 1 * 1024 * 1024 # 目标 websocket 写缓冲超过该值时暂停读取来源 websocket
|
|
34
34
|
WRITE_DRAIN_TIMEOUT = 30
|
|
35
35
|
|
|
36
36
|
|
|
@@ -136,8 +136,12 @@ class MyWebSocketaHandler(WebSocketHandler):
|
|
|
136
136
|
写缓冲积压超过高水位时返回 future, 调用方 await 它做背压; 否则返回 None
|
|
137
137
|
"""
|
|
138
138
|
message = NatSerialization.dumps(message_dict, ContextUtils.get_password(), self.compress_support, self.protocol_version)
|
|
139
|
+
return self.write_bytes(bytes(message))
|
|
140
|
+
|
|
141
|
+
def write_bytes(self, message: bytes) -> Optional[asyncio.Future]:
|
|
142
|
+
"""写入已序列化的消息, 不等待 flush; 写缓冲积压超过高水位时返回 future, 否则返回 None"""
|
|
139
143
|
try:
|
|
140
|
-
future = self._write_nowait(
|
|
144
|
+
future = self._write_nowait(message)
|
|
141
145
|
except WebSocketClosedError:
|
|
142
146
|
LoggerFactory.get_logger().info(f'forward to closed websocket {self.client_name}, drop')
|
|
143
147
|
return None
|
|
@@ -402,24 +406,11 @@ class MyWebSocketaHandler(WebSocketHandler):
|
|
|
402
406
|
connection = tcp_forward_client.uid_to_connection[uid]
|
|
403
407
|
with connection.early_data_lock:
|
|
404
408
|
connection.connection_confirmed = True
|
|
405
|
-
|
|
406
|
-
connection.early_data_buffer = []
|
|
409
|
+
early_data_bytes = connection.early_data_bytes
|
|
407
410
|
LoggerFactory.get_logger().info(
|
|
408
|
-
f'connect confirmed uid: {uid.hex()}, flushing {
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
for buffered_data in early_data_buffer:
|
|
412
|
-
if buffered_data:
|
|
413
|
-
send_message: MessageEntity = {
|
|
414
|
-
'type_': MessageTypeConstant.WEBSOCKET_OVER_TCP,
|
|
415
|
-
'data': {
|
|
416
|
-
'name': connection.socket_server.name,
|
|
417
|
-
'data': buffered_data,
|
|
418
|
-
'uid': uid,
|
|
419
|
-
'ip_port': connection.socket_server.ip_port
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
self.forward_message(send_message)
|
|
411
|
+
f'connect confirmed uid: {uid.hex()}, flushing {early_data_bytes} bytes, {connection.stats()}')
|
|
412
|
+
# 单独的任务 flush: 背压等待不能阻塞本 websocket 后续消息的处理
|
|
413
|
+
asyncio.ensure_future(tcp_forward_client.flush_early_data(connection))
|
|
423
414
|
|
|
424
415
|
elif message_dict['type_'] == MessageTypeConstant.CONNECT_FAILED:
|
|
425
416
|
data: TcpOverWebsocketMessage = message_dict['data']
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/compliance/autobahn-test-report-Feb-03-2021.html
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{proxynt-2.1.0 → proxynt-2.1.1}/common/websocket2/websocket/tests/test_handshake_large_response.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|