proxynt 2.1.0rc1__tar.gz → 2.1.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.
Files changed (152) hide show
  1. {proxynt-2.1.0rc1/proxynt.egg-info → proxynt-2.1.2}/PKG-INFO +1 -6
  2. proxynt-2.1.2/common/captcha.py +123 -0
  3. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/pool.py +22 -12
  4. {proxynt-2.1.0rc1 → proxynt-2.1.2}/constant/system_constant.py +1 -1
  5. {proxynt-2.1.0rc1 → proxynt-2.1.2/proxynt.egg-info}/PKG-INFO +1 -6
  6. {proxynt-2.1.0rc1 → proxynt-2.1.2}/proxynt.egg-info/SOURCES.txt +1 -0
  7. proxynt-2.1.2/proxynt.egg-info/entry_points.txt +3 -0
  8. {proxynt-2.1.0rc1 → proxynt-2.1.2}/run_client.py +2 -0
  9. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/admin_http_handler.py +60 -5
  10. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/tcp_forward_client.py +67 -20
  11. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/template/login.html +19 -2
  12. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/websocket_handler.py +12 -19
  13. proxynt-2.1.0rc1/proxynt.egg-info/entry_points.txt +0 -5
  14. {proxynt-2.1.0rc1 → proxynt-2.1.2}/LICENSE +0 -0
  15. {proxynt-2.1.0rc1 → proxynt-2.1.2}/MANIFEST.in +0 -0
  16. {proxynt-2.1.0rc1 → proxynt-2.1.2}/__init__.py +0 -0
  17. {proxynt-2.1.0rc1 → proxynt-2.1.2}/client/__init__.py +0 -0
  18. {proxynt-2.1.0rc1 → proxynt-2.1.2}/client/abstract_tunnel.py +0 -0
  19. {proxynt-2.1.0rc1 → proxynt-2.1.2}/client/clear_nonce_task.py +0 -0
  20. {proxynt-2.1.0rc1 → proxynt-2.1.2}/client/heart_beat_task.py +0 -0
  21. {proxynt-2.1.0rc1 → proxynt-2.1.2}/client/kcp_tunnel_impl.py +0 -0
  22. {proxynt-2.1.0rc1 → proxynt-2.1.2}/client/n4_tunnel_manager.py +0 -0
  23. {proxynt-2.1.0rc1 → proxynt-2.1.2}/client/quic_tunnel_impl.py +0 -0
  24. {proxynt-2.1.0rc1 → proxynt-2.1.2}/client/tcp_forward_client.py +0 -0
  25. {proxynt-2.1.0rc1 → proxynt-2.1.2}/client/tunnel_protocol.py +0 -0
  26. {proxynt-2.1.0rc1 → proxynt-2.1.2}/client/udp_forward_client.py +0 -0
  27. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/__init__.py +0 -0
  28. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/cert_utils.py +0 -0
  29. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/crypto/__init__.py +0 -0
  30. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/crypto/table.py +0 -0
  31. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/encrypt_utils.py +0 -0
  32. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/kcp.py +0 -0
  33. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/logger_factory.py +0 -0
  34. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/n4_protocol.py +0 -0
  35. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/n4_punch.py +0 -0
  36. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/nat_serialization.py +0 -0
  37. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/nat_serialization_v1.py +0 -0
  38. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/nat_serialization_v2.py +0 -0
  39. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/register_append_data.py +0 -0
  40. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/speed_limit.py +0 -0
  41. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/__init__.py +0 -0
  42. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/_abnf.py +0 -0
  43. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/_app.py +0 -0
  44. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/_cookiejar.py +0 -0
  45. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/_core.py +0 -0
  46. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/_dispatcher.py +0 -0
  47. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/_exceptions.py +0 -0
  48. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/_handshake.py +0 -0
  49. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/_http.py +0 -0
  50. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/_logging.py +0 -0
  51. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/_socket.py +0 -0
  52. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/_ssl_compat.py +0 -0
  53. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/_url.py +0 -0
  54. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/_utils.py +0 -0
  55. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/_wsdump.py +0 -0
  56. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/tests/__init__.py +0 -0
  57. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/tests/echo-server.py +0 -0
  58. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/tests/test_abnf.py +0 -0
  59. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/tests/test_app.py +0 -0
  60. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/tests/test_cookiejar.py +0 -0
  61. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/tests/test_dispatcher.py +0 -0
  62. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/tests/test_handshake_large_response.py +0 -0
  63. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/tests/test_http.py +0 -0
  64. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/tests/test_large_payloads.py +0 -0
  65. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/tests/test_logging_helpers.py +0 -0
  66. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/tests/test_reconnect_bad_fd.py +0 -0
  67. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/tests/test_socket.py +0 -0
  68. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/tests/test_socket_bugs.py +0 -0
  69. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/tests/test_ssl_compat.py +0 -0
  70. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/tests/test_ssl_edge_cases.py +0 -0
  71. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/tests/test_url.py +0 -0
  72. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/tests/test_utils.py +0 -0
  73. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket/tests/test_websocket.py +0 -0
  74. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/__init__.py +0 -0
  75. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/compliance/autobahn-test-report-Feb-03-2021.html +0 -0
  76. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/setup.py +0 -0
  77. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/__init__.py +0 -0
  78. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/_abnf.py +0 -0
  79. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/_app.py +0 -0
  80. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/_cookiejar.py +0 -0
  81. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/_core.py +0 -0
  82. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/_dispatcher.py +0 -0
  83. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/_exceptions.py +0 -0
  84. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/_handshake.py +0 -0
  85. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/_http.py +0 -0
  86. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/_logging.py +0 -0
  87. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/_socket.py +0 -0
  88. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/_ssl_compat.py +0 -0
  89. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/_url.py +0 -0
  90. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/_utils.py +0 -0
  91. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/_wsdump.py +0 -0
  92. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/tests/__init__.py +0 -0
  93. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/tests/echo-server.py +0 -0
  94. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/tests/test_abnf.py +0 -0
  95. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/tests/test_app.py +0 -0
  96. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/tests/test_cookiejar.py +0 -0
  97. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/tests/test_dispatcher.py +0 -0
  98. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/tests/test_handshake_large_response.py +0 -0
  99. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/tests/test_http.py +0 -0
  100. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/tests/test_large_payloads.py +0 -0
  101. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/tests/test_logging_helpers.py +0 -0
  102. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/tests/test_reconnect_bad_fd.py +0 -0
  103. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/tests/test_socket.py +0 -0
  104. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/tests/test_socket_bugs.py +0 -0
  105. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/tests/test_ssl_compat.py +0 -0
  106. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/tests/test_ssl_edge_cases.py +0 -0
  107. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/tests/test_url.py +0 -0
  108. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/tests/test_utils.py +0 -0
  109. {proxynt-2.1.0rc1 → proxynt-2.1.2}/common/websocket2/websocket/tests/test_websocket.py +0 -0
  110. {proxynt-2.1.0rc1 → proxynt-2.1.2}/constant/__init__.py +0 -0
  111. {proxynt-2.1.0rc1 → proxynt-2.1.2}/constant/message_type_constnat.py +0 -0
  112. {proxynt-2.1.0rc1 → proxynt-2.1.2}/context/__init__.py +0 -0
  113. {proxynt-2.1.0rc1 → proxynt-2.1.2}/context/context_utils.py +0 -0
  114. {proxynt-2.1.0rc1 → proxynt-2.1.2}/entity/__init__.py +0 -0
  115. {proxynt-2.1.0rc1 → proxynt-2.1.2}/entity/client_config_entity.py +0 -0
  116. {proxynt-2.1.0rc1 → proxynt-2.1.2}/entity/message/__init__.py +0 -0
  117. {proxynt-2.1.0rc1 → proxynt-2.1.2}/entity/message/message_entity.py +0 -0
  118. {proxynt-2.1.0rc1 → proxynt-2.1.2}/entity/message/push_config_entity.py +0 -0
  119. {proxynt-2.1.0rc1 → proxynt-2.1.2}/entity/message/tcp_over_websocket_message.py +0 -0
  120. {proxynt-2.1.0rc1 → proxynt-2.1.2}/entity/server_config_entity.py +0 -0
  121. {proxynt-2.1.0rc1 → proxynt-2.1.2}/exceptions/__init__.py +0 -0
  122. {proxynt-2.1.0rc1 → proxynt-2.1.2}/exceptions/duplicated_name.py +0 -0
  123. {proxynt-2.1.0rc1 → proxynt-2.1.2}/exceptions/invalid_password.py +0 -0
  124. {proxynt-2.1.0rc1 → proxynt-2.1.2}/exceptions/replay_error.py +0 -0
  125. {proxynt-2.1.0rc1 → proxynt-2.1.2}/exceptions/signature_error.py +0 -0
  126. {proxynt-2.1.0rc1 → proxynt-2.1.2}/p2ptest/__init__.py +0 -0
  127. {proxynt-2.1.0rc1 → proxynt-2.1.2}/p2ptest/client.py +0 -0
  128. {proxynt-2.1.0rc1 → proxynt-2.1.2}/p2ptest/n4.py +0 -0
  129. {proxynt-2.1.0rc1 → proxynt-2.1.2}/p2ptest/tls_handshake_timeout_test.py +0 -0
  130. {proxynt-2.1.0rc1 → proxynt-2.1.2}/proxynt.egg-info/dependency_links.txt +0 -0
  131. {proxynt-2.1.0rc1 → proxynt-2.1.2}/proxynt.egg-info/requires.txt +0 -0
  132. {proxynt-2.1.0rc1 → proxynt-2.1.2}/proxynt.egg-info/top_level.txt +0 -0
  133. {proxynt-2.1.0rc1 → proxynt-2.1.2}/run_server.py +0 -0
  134. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/__init__.py +0 -0
  135. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/n4.py +0 -0
  136. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/n4_signal_service.py +0 -0
  137. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/task/__init__.py +0 -0
  138. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/task/check_cookie_task.py +0 -0
  139. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/task/clear_nonce_task.py +0 -0
  140. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/task/heart_beat_task.py +0 -0
  141. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/template/__init__.py +0 -0
  142. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/template/base.html +0 -0
  143. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/template/css/fonts/element-icons.woff +0 -0
  144. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/template/css/index.css +0 -0
  145. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/template/ele_index.html +0 -0
  146. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/template/js/axios.min.js +0 -0
  147. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/template/js/index.js +0 -0
  148. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/template/js/vue.min.js +0 -0
  149. {proxynt-2.1.0rc1 → proxynt-2.1.2}/server/udp_forward_client.py +0 -0
  150. {proxynt-2.1.0rc1 → proxynt-2.1.2}/setup.cfg +0 -0
  151. {proxynt-2.1.0rc1 → proxynt-2.1.2}/setup.py +0 -0
  152. {proxynt-2.1.0rc1 → proxynt-2.1.2}/test_exchange.py +0 -0
@@ -1,10 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: proxynt
3
- Version: 2.1.0rc1
4
- Summary: UNKNOWN
3
+ Version: 2.1.2
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
- """暂停监听可读事件 (modify 掩码为 0)"""
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
- reasons = self._pause_reasons.setdefault(s, set())
86
- first_pause = not reasons
87
- reasons.add(reason)
88
- if first_pause:
89
- try:
90
- self.selector.modify(s, 0, key.data)
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
- key = self.selector.get_key(s)
106
- self.selector.modify(s, EVENT_READ, key.data)
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
 
@@ -15,7 +15,7 @@ class SystemConstant:
15
15
 
16
16
  COOKIE_EXPIRE_SECONDS = 3600 * 24
17
17
 
18
- VERSION = '2.1.0.rc1'
18
+ VERSION = '2.1.2'
19
19
 
20
20
  GITHUB = 'https://github.com/sazima/proxynt'
21
21
 
@@ -1,10 +1,7 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: proxynt
3
- Version: 2.1.0rc1
4
- Summary: UNKNOWN
3
+ Version: 2.1.2
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
-
@@ -16,6 +16,7 @@ setup.py
16
16
  ./client/tunnel_protocol.py
17
17
  ./client/udp_forward_client.py
18
18
  ./common/__init__.py
19
+ ./common/captcha.py
19
20
  ./common/cert_utils.py
20
21
  ./common/encrypt_utils.py
21
22
  ./common/kcp.py
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ nt_client = proxynt.run_client:main
3
+ nt_server = proxynt.run_server:main
@@ -402,6 +402,8 @@ async def connect_loop(url, client, processor):
402
402
  on_message_callback=on_message,
403
403
  connect_timeout=10,
404
404
  )
405
+ # 关闭 Nagle: 否则小包要等上一个包的 ACK, 每个方向多出约一个 RTT 的延迟
406
+ ws_conn.protocol.set_nodelay(True)
405
407
  client.ws_sender.set_connection(ws_conn, io)
406
408
  client.on_open()
407
409
  await close_future
@@ -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
- result = self.get_cookie(COOKIE_KEY)
27
- cookie_dict = ContextUtils.get_cookie_to_time()
28
- if result in cookie_dict and time.time() - cookie_dict[result] < SystemConstant.COOKIE_EXPIRE_SECONDS:
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['password']
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.max_early_data_packets = 65536 * 4 # Maximum buffered packets
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: buffer data if connection not confirmed
222
- if socket_connection.optimistic_mode and not socket_connection.connection_confirmed:
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.connection_confirmed:
226
- if not recv:
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
- if len(socket_connection.early_data_buffer) < socket_connection.max_early_data_packets:
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
- self.tornado_loop.add_callback(
275
- partial(handler.write_message, NatSerialization.dumps(send_message, ContextUtils.get_password(), is_compress, protocol_version)), True)
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 = 4 * 1024 * 1024 # 目标 websocket 写缓冲超过该值时暂停读取来源 websocket
33
+ WRITE_BUFFER_HIGH_WATER = 1 * 1024 * 1024 # 目标 websocket 写缓冲超过该值时暂停读取来源 websocket
34
34
  WRITE_DRAIN_TIMEOUT = 30
35
35
 
36
36
 
@@ -63,6 +63,8 @@ class MyWebSocketaHandler(WebSocketHandler):
63
63
  cls.c2c_lock = asyncio.Lock()
64
64
 
65
65
  def open(self, *args: str, **kwargs: str):
66
+ # 关闭 Nagle: 否则小包要等上一个包的 ACK, 每个方向多出约一个 RTT 的延迟
67
+ self.set_nodelay(True)
66
68
  self.lock = Lock()
67
69
  self._pending_write_bytes = 0
68
70
  self.client_name = None
@@ -136,8 +138,12 @@ class MyWebSocketaHandler(WebSocketHandler):
136
138
  写缓冲积压超过高水位时返回 future, 调用方 await 它做背压; 否则返回 None
137
139
  """
138
140
  message = NatSerialization.dumps(message_dict, ContextUtils.get_password(), self.compress_support, self.protocol_version)
141
+ return self.write_bytes(bytes(message))
142
+
143
+ def write_bytes(self, message: bytes) -> Optional[asyncio.Future]:
144
+ """写入已序列化的消息, 不等待 flush; 写缓冲积压超过高水位时返回 future, 否则返回 None"""
139
145
  try:
140
- future = self._write_nowait(bytes(message))
146
+ future = self._write_nowait(message)
141
147
  except WebSocketClosedError:
142
148
  LoggerFactory.get_logger().info(f'forward to closed websocket {self.client_name}, drop')
143
149
  return None
@@ -402,24 +408,11 @@ class MyWebSocketaHandler(WebSocketHandler):
402
408
  connection = tcp_forward_client.uid_to_connection[uid]
403
409
  with connection.early_data_lock:
404
410
  connection.connection_confirmed = True
405
- early_data_buffer = connection.early_data_buffer
406
- connection.early_data_buffer = []
411
+ early_data_bytes = connection.early_data_bytes
407
412
  LoggerFactory.get_logger().info(
408
- f'connect confirmed uid: {uid.hex()}, flushing {sum(len(b) for b in early_data_buffer)} bytes, {connection.stats()}')
409
-
410
- # 同步写入不 await, 保证早期数据排在 socket 线程后续数据之前
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)
413
+ f'connect confirmed uid: {uid.hex()}, flushing {early_data_bytes} bytes, {connection.stats()}')
414
+ # 单独的任务 flush: 背压等待不能阻塞本 websocket 后续消息的处理
415
+ asyncio.ensure_future(tcp_forward_client.flush_early_data(connection))
423
416
 
424
417
  elif message_dict['type_'] == MessageTypeConstant.CONNECT_FAILED:
425
418
  data: TcpOverWebsocketMessage = message_dict['data']
@@ -1,5 +0,0 @@
1
-
2
- [console_scripts]
3
- nt_client = proxynt.run_client:main
4
- nt_server = proxynt.run_server:main
5
-
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