kite-strings 0.1.0__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,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fuqingxu
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: kite-strings
3
+ Version: 0.1.0
4
+ Summary: Expose web services on internal servers through a cloud relay with a dashboard and reverse proxy over WebSocket tunnels.
5
+ Author: fuqingxu
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/binary-husky/kite-string
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: aiohttp>=3.9
12
+ Dynamic: license-file
13
+
14
+ # Kite - 反向隧道服务注册系统
15
+
16
+ 将内网服务器上的 Web 服务通过云服务器暴露给外部访问。
17
+
18
+ ## 适用场景
19
+
20
+ ```
21
+ ┌─────────┐ WebSocket ┌─────────────┐ HTTP ┌─────────┐
22
+ │ 内网服务器├──────────────►│ 云服务器 X │◄─────────│ 浏览器 │
23
+ │ A/B/C │ │ kite-server │ └─────────┘
24
+ │(kite-client) │ Dashboard │
25
+ └─────────┘ │ + 反向代理 │
26
+ └─────────────┘
27
+ ```
28
+
29
+ - 内网服务器 A/B/C 无法从外部直接访问
30
+ - A/B/C 可以主动连接云服务器 X,反之不行
31
+ - 需要通过 X 访问 A/B/C 上的 Web 服务
32
+
33
+ ## 安装
34
+
35
+ Python 3.8+,唯一依赖 `aiohttp`:
36
+
37
+ ```bash
38
+ pip install aiohttp
39
+ ```
40
+
41
+ ## 使用
42
+
43
+ ### 1. 在云服务器 X 上启动 Server
44
+
45
+ ```bash
46
+ python kite_server.py --port 8080
47
+ ```
48
+
49
+ 可选参数:
50
+
51
+ | 参数 | 默认值 | 说明 |
52
+ |------|--------|------|
53
+ | `--host` | `0.0.0.0` | 监听地址 |
54
+ | `--port` | `8080` | 监听端口 |
55
+
56
+ ### 2. 在内网服务器上启动 Client
57
+
58
+ 假设 A 上有一个运行在 3000 端口的 Web 服务:
59
+
60
+ ```bash
61
+ python kite_client.py --server ws://X:8080/ws --port 3000 --name "my-api"
62
+ ```
63
+
64
+ 可选参数:
65
+
66
+ | 参数 | 必填 | 默认值 | 说明 |
67
+ |------|------|--------|------|
68
+ | `--server` | 是 | - | Server 的 WebSocket 地址 |
69
+ | `--port` | 是 | - | 本地 Web 服务端口 |
70
+ | `--name` | 否 | `主机名:端口` | 服务显示名称 |
71
+
72
+ ### 3. 访问
73
+
74
+ - **Dashboard**:浏览器打开 `http://X:8080`,查看所有已注册服务
75
+ - **代理访问**:点击服务名称,或直接访问 `http://X:8080/proxy/{service_id}/`
76
+
77
+ ## 工作原理
78
+
79
+ 1. Client 通过 WebSocket 连接 Server,注册本地服务信息
80
+ 2. Server 记录服务并在 Dashboard 展示
81
+ 3. 浏览器访问代理路径时,Server 将 HTTP 请求通过 WebSocket 转发给 Client
82
+ 4. Client 向本地服务发起请求,将响应原路返回
83
+ 5. Client 断线后自动重连(指数退避,1s → 30s)
84
+
85
+ ## 示例
86
+
87
+ 在同一台机器上快速体验:
88
+
89
+ ```bash
90
+ # 终端 1:启动 Server
91
+ python kite_server.py --port 8080
92
+
93
+ # 终端 2:启动一个测试 Web 服务
94
+ python -m http.server 9000
95
+
96
+ # 终端 3:启动 Client,将 9000 端口暴露到 Server
97
+ python kite_client.py --server ws://localhost:8080/ws --port 9000 --name "file-server"
98
+
99
+ # 浏览器打开 http://localhost:8080 查看 Dashboard,点击服务即可访问
100
+ ```
101
+
102
+ ## 多服务注册
103
+
104
+ 每个 Web 服务启动一个 Client 实例即可:
105
+
106
+ ```bash
107
+ # Server A
108
+ python kite_client.py --server ws://X:8080/ws --port 3000 --name "api-server"
109
+ python kite_client.py --server ws://X:8080/ws --port 8888 --name "jupyter"
110
+
111
+ # Server B
112
+ python kite_client.py --server ws://X:8080/ws --port 5000 --name "flask-app"
113
+ ```
@@ -0,0 +1,100 @@
1
+ # Kite - 反向隧道服务注册系统
2
+
3
+ 将内网服务器上的 Web 服务通过云服务器暴露给外部访问。
4
+
5
+ ## 适用场景
6
+
7
+ ```
8
+ ┌─────────┐ WebSocket ┌─────────────┐ HTTP ┌─────────┐
9
+ │ 内网服务器├──────────────►│ 云服务器 X │◄─────────│ 浏览器 │
10
+ │ A/B/C │ │ kite-server │ └─────────┘
11
+ │(kite-client) │ Dashboard │
12
+ └─────────┘ │ + 反向代理 │
13
+ └─────────────┘
14
+ ```
15
+
16
+ - 内网服务器 A/B/C 无法从外部直接访问
17
+ - A/B/C 可以主动连接云服务器 X,反之不行
18
+ - 需要通过 X 访问 A/B/C 上的 Web 服务
19
+
20
+ ## 安装
21
+
22
+ Python 3.8+,唯一依赖 `aiohttp`:
23
+
24
+ ```bash
25
+ pip install aiohttp
26
+ ```
27
+
28
+ ## 使用
29
+
30
+ ### 1. 在云服务器 X 上启动 Server
31
+
32
+ ```bash
33
+ python kite_server.py --port 8080
34
+ ```
35
+
36
+ 可选参数:
37
+
38
+ | 参数 | 默认值 | 说明 |
39
+ |------|--------|------|
40
+ | `--host` | `0.0.0.0` | 监听地址 |
41
+ | `--port` | `8080` | 监听端口 |
42
+
43
+ ### 2. 在内网服务器上启动 Client
44
+
45
+ 假设 A 上有一个运行在 3000 端口的 Web 服务:
46
+
47
+ ```bash
48
+ python kite_client.py --server ws://X:8080/ws --port 3000 --name "my-api"
49
+ ```
50
+
51
+ 可选参数:
52
+
53
+ | 参数 | 必填 | 默认值 | 说明 |
54
+ |------|------|--------|------|
55
+ | `--server` | 是 | - | Server 的 WebSocket 地址 |
56
+ | `--port` | 是 | - | 本地 Web 服务端口 |
57
+ | `--name` | 否 | `主机名:端口` | 服务显示名称 |
58
+
59
+ ### 3. 访问
60
+
61
+ - **Dashboard**:浏览器打开 `http://X:8080`,查看所有已注册服务
62
+ - **代理访问**:点击服务名称,或直接访问 `http://X:8080/proxy/{service_id}/`
63
+
64
+ ## 工作原理
65
+
66
+ 1. Client 通过 WebSocket 连接 Server,注册本地服务信息
67
+ 2. Server 记录服务并在 Dashboard 展示
68
+ 3. 浏览器访问代理路径时,Server 将 HTTP 请求通过 WebSocket 转发给 Client
69
+ 4. Client 向本地服务发起请求,将响应原路返回
70
+ 5. Client 断线后自动重连(指数退避,1s → 30s)
71
+
72
+ ## 示例
73
+
74
+ 在同一台机器上快速体验:
75
+
76
+ ```bash
77
+ # 终端 1:启动 Server
78
+ python kite_server.py --port 8080
79
+
80
+ # 终端 2:启动一个测试 Web 服务
81
+ python -m http.server 9000
82
+
83
+ # 终端 3:启动 Client,将 9000 端口暴露到 Server
84
+ python kite_client.py --server ws://localhost:8080/ws --port 9000 --name "file-server"
85
+
86
+ # 浏览器打开 http://localhost:8080 查看 Dashboard,点击服务即可访问
87
+ ```
88
+
89
+ ## 多服务注册
90
+
91
+ 每个 Web 服务启动一个 Client 实例即可:
92
+
93
+ ```bash
94
+ # Server A
95
+ python kite_client.py --server ws://X:8080/ws --port 3000 --name "api-server"
96
+ python kite_client.py --server ws://X:8080/ws --port 8888 --name "jupyter"
97
+
98
+ # Server B
99
+ python kite_client.py --server ws://X:8080/ws --port 5000 --name "flask-app"
100
+ ```
@@ -0,0 +1,3 @@
1
+ """Kite String - Reverse tunnel service registry and proxy."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,139 @@
1
+ #!/usr/bin/env python3
2
+ """Kite Client - Register a local web service through a Kite server tunnel."""
3
+
4
+ import argparse
5
+ import asyncio
6
+ import base64
7
+ import json
8
+ import platform
9
+ import signal
10
+ import sys
11
+
12
+ import aiohttp
13
+
14
+ # ---------------------------------------------------------------------------
15
+ # Tunnel client
16
+ # ---------------------------------------------------------------------------
17
+
18
+ async def run_tunnel(server_url: str, local_port: int, name: str):
19
+ """Connect to Kite server and serve proxy requests."""
20
+ backoff = 1
21
+ max_backoff = 30
22
+ host = platform.node()
23
+
24
+ while True:
25
+ try:
26
+ async with aiohttp.ClientSession() as session:
27
+ print(f"Connecting to {server_url} ...")
28
+ async with session.ws_connect(server_url, heartbeat=20) as ws:
29
+ # Register
30
+ await ws.send_json({
31
+ "type": "register",
32
+ "name": name,
33
+ "port": local_port,
34
+ "host": host,
35
+ })
36
+
37
+ reg = await ws.receive_json()
38
+ service_id = reg.get("id", "?")
39
+ print(f"Registered as '{name}' (id={service_id}), tunneling localhost:{local_port}")
40
+ backoff = 1 # reset on successful connect
41
+
42
+ async for msg in ws:
43
+ if msg.type == aiohttp.WSMsgType.TEXT:
44
+ data = json.loads(msg.data)
45
+ if data.get("type") == "request":
46
+ asyncio.create_task(
47
+ handle_request(ws, data, local_port)
48
+ )
49
+ elif msg.type in (aiohttp.WSMsgType.ERROR, aiohttp.WSMsgType.CLOSE):
50
+ break
51
+
52
+ except (aiohttp.ClientError, OSError, asyncio.TimeoutError) as e:
53
+ print(f"Connection lost: {e}")
54
+
55
+ print(f"Reconnecting in {backoff}s ...")
56
+ await asyncio.sleep(backoff)
57
+ backoff = min(backoff * 2, max_backoff)
58
+
59
+
60
+ async def handle_request(ws, data: dict, local_port: int):
61
+ """Forward a proxied request to the local service and return the response."""
62
+ req_id = data["id"]
63
+ method = data.get("method", "GET")
64
+ path = data.get("path", "/")
65
+ query = data.get("query", "")
66
+ headers = data.get("headers", {})
67
+ body_b64 = data.get("body", "")
68
+
69
+ url = f"http://127.0.0.1:{local_port}{path}"
70
+ if query:
71
+ url += f"?{query}"
72
+
73
+ body = base64.b64decode(body_b64) if body_b64 else None
74
+
75
+ try:
76
+ async with aiohttp.ClientSession() as session:
77
+ async with session.request(
78
+ method, url, headers=headers, data=body, timeout=aiohttp.ClientTimeout(total=25)
79
+ ) as resp:
80
+ resp_body = await resp.read()
81
+ resp_headers = {k: v for k, v in resp.headers.items()
82
+ if k.lower() not in ("transfer-encoding", "connection", "keep-alive")}
83
+
84
+ await ws.send_json({
85
+ "type": "response",
86
+ "id": req_id,
87
+ "status": resp.status,
88
+ "headers": resp_headers,
89
+ "body": base64.b64encode(resp_body).decode(),
90
+ })
91
+ except Exception as e:
92
+ print(f"Error proxying request {req_id}: {e}")
93
+ try:
94
+ await ws.send_json({
95
+ "type": "response",
96
+ "id": req_id,
97
+ "status": 502,
98
+ "headers": {"content-type": "text/plain"},
99
+ "body": base64.b64encode(f"Proxy error: {e}".encode()).decode(),
100
+ })
101
+ except Exception:
102
+ pass
103
+
104
+
105
+ # ---------------------------------------------------------------------------
106
+ # Main
107
+ # ---------------------------------------------------------------------------
108
+
109
+ def main():
110
+ parser = argparse.ArgumentParser(description="Kite Client")
111
+ parser.add_argument("--server", required=True, help="Kite server WebSocket URL (e.g. ws://X:8080/ws)")
112
+ parser.add_argument("--port", type=int, required=True, help="Local port to expose")
113
+ parser.add_argument("--name", default="", help="Service name (default: hostname:port)")
114
+ args = parser.parse_args()
115
+
116
+ name = args.name or f"{platform.node()}:{args.port}"
117
+
118
+ loop = asyncio.new_event_loop()
119
+
120
+ def shutdown():
121
+ print("\nShutting down...")
122
+ for task in asyncio.all_tasks(loop):
123
+ task.cancel()
124
+
125
+ if sys.platform != "win32":
126
+ loop.add_signal_handler(signal.SIGINT, shutdown)
127
+ loop.add_signal_handler(signal.SIGTERM, shutdown)
128
+
129
+ try:
130
+ loop.run_until_complete(run_tunnel(args.server, args.port, name))
131
+ except (KeyboardInterrupt, asyncio.CancelledError):
132
+ pass
133
+ finally:
134
+ loop.close()
135
+ print("Bye.")
136
+
137
+
138
+ if __name__ == "__main__":
139
+ main()
@@ -0,0 +1,227 @@
1
+ #!/usr/bin/env python3
2
+ """Kite Server - Reverse tunnel service registry and proxy."""
3
+
4
+ import argparse
5
+ import asyncio
6
+ import base64
7
+ import json
8
+ import time
9
+ import uuid
10
+
11
+ from aiohttp import web, WSMsgType
12
+
13
+ # ---------------------------------------------------------------------------
14
+ # State
15
+ # ---------------------------------------------------------------------------
16
+
17
+ services: dict[str, dict] = {} # service_id -> service info
18
+ pending: dict[str, asyncio.Future] = {} # request_id -> Future[response_msg]
19
+
20
+ # ---------------------------------------------------------------------------
21
+ # Dashboard HTML (embedded)
22
+ # ---------------------------------------------------------------------------
23
+
24
+ DASHBOARD_HTML = """\
25
+ <!DOCTYPE html>
26
+ <html lang="en">
27
+ <head>
28
+ <meta charset="utf-8">
29
+ <title>Kite Dashboard</title>
30
+ <style>
31
+ * { box-sizing: border-box; margin: 0; padding: 0; }
32
+ body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
33
+ background: #f5f7fa; color: #333; padding: 2rem; }
34
+ h1 { margin-bottom: 1.5rem; font-size: 1.6rem; }
35
+ .empty { color: #999; font-style: italic; }
36
+ table { width: 100%; border-collapse: collapse; background: #fff;
37
+ border-radius: 8px; overflow: hidden; box-shadow: 0 1px 3px rgba(0,0,0,.1); }
38
+ th, td { text-align: left; padding: .75rem 1rem; }
39
+ th { background: #4a90d9; color: #fff; font-weight: 600; }
40
+ tr:nth-child(even) { background: #f9fafb; }
41
+ tr:hover { background: #eef3fb; }
42
+ a { color: #4a90d9; text-decoration: none; font-weight: 500; }
43
+ a:hover { text-decoration: underline; }
44
+ .status { display: inline-block; width: 10px; height: 10px; border-radius: 50%;
45
+ background: #4caf50; margin-right: .4rem; vertical-align: middle; }
46
+ .status.offline { background: #ccc; }
47
+ .refresh-note { margin-top: 1rem; font-size: .85rem; color: #999; }
48
+ </style>
49
+ </head>
50
+ <body>
51
+ <h1>Kite Dashboard</h1>
52
+ <div id="content"><p class="empty">Loading...</p></div>
53
+ <p class="refresh-note">Auto-refreshes every 5 seconds</p>
54
+ <script>
55
+ async function load() {
56
+ try {
57
+ const res = await fetch('/api/services');
58
+ const data = await res.json();
59
+ if (data.length === 0) {
60
+ document.getElementById('content').innerHTML = '<p class="empty">No services registered.</p>';
61
+ return;
62
+ }
63
+ let html = '<table><tr><th></th><th>Name</th><th>Host</th><th>Port</th><th>Registered</th></tr>';
64
+ for (const s of data) {
65
+ const t = new Date(s.registered_at * 1000).toLocaleString();
66
+ html += `<tr>
67
+ <td><span class="status ${s.online ? '' : 'offline'}"></span></td>
68
+ <td><a href="/proxy/${s.id}/" target="_blank">${esc(s.name)}</a></td>
69
+ <td>${esc(s.host)}</td>
70
+ <td>${s.port}</td>
71
+ <td>${t}</td>
72
+ </tr>`;
73
+ }
74
+ html += '</table>';
75
+ document.getElementById('content').innerHTML = html;
76
+ } catch(e) { console.error(e); }
77
+ }
78
+ function esc(s) { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; }
79
+ load();
80
+ setInterval(load, 5000);
81
+ </script>
82
+ </body>
83
+ </html>
84
+ """
85
+
86
+ # ---------------------------------------------------------------------------
87
+ # Handlers
88
+ # ---------------------------------------------------------------------------
89
+
90
+ async def handle_dashboard(request: web.Request) -> web.Response:
91
+ return web.Response(text=DASHBOARD_HTML, content_type="text/html")
92
+
93
+
94
+ async def handle_api_services(request: web.Request) -> web.Response:
95
+ items = sorted(services.values(), key=lambda s: s["registered_at"], reverse=True)
96
+ out = [
97
+ {
98
+ "id": s["id"],
99
+ "name": s["name"],
100
+ "host": s["host"],
101
+ "port": s["port"],
102
+ "registered_at": s["registered_at"],
103
+ "online": s.get("online", False),
104
+ }
105
+ for s in items
106
+ ]
107
+ return web.json_response(out)
108
+
109
+
110
+ async def handle_ws(request: web.Request) -> web.WebSocketResponse:
111
+ ws = web.WebSocketResponse(heartbeat=20)
112
+ await ws.prepare(request)
113
+
114
+ service_id = None
115
+
116
+ try:
117
+ async for msg in ws:
118
+ if msg.type == WSMsgType.TEXT:
119
+ data = json.loads(msg.data)
120
+
121
+ if data.get("type") == "register":
122
+ service_id = str(uuid.uuid4())[:8]
123
+ services[service_id] = {
124
+ "id": service_id,
125
+ "name": data.get("name", "unnamed"),
126
+ "host": data.get("host", "unknown"),
127
+ "port": data.get("port", 0),
128
+ "registered_at": time.time(),
129
+ "online": True,
130
+ "ws": ws,
131
+ }
132
+ await ws.send_json({"type": "registered", "id": service_id})
133
+ print(f"[+] Service registered: {services[service_id]['name']} "
134
+ f"({services[service_id]['host']}:{data.get('port')}) -> {service_id}")
135
+
136
+ elif data.get("type") == "response":
137
+ req_id = data.get("id")
138
+ fut = pending.pop(req_id, None)
139
+ if fut and not fut.done():
140
+ fut.set_result(data)
141
+
142
+ elif msg.type in (WSMsgType.ERROR, WSMsgType.CLOSE):
143
+ break
144
+ finally:
145
+ if service_id and service_id in services:
146
+ services[service_id]["online"] = False
147
+ services[service_id].pop("ws", None)
148
+ print(f"[-] Service disconnected: {services[service_id]['name']} ({service_id})")
149
+
150
+ return ws
151
+
152
+
153
+ async def handle_proxy(request: web.Request) -> web.Response:
154
+ service_id = request.match_info["service_id"]
155
+ subpath = request.match_info.get("path", "")
156
+
157
+ svc = services.get(service_id)
158
+ if not svc or not svc.get("online") or not svc.get("ws"):
159
+ return web.Response(status=502, text="Service not available")
160
+
161
+ # Read request body
162
+ body_bytes = await request.read()
163
+ body_b64 = base64.b64encode(body_bytes).decode() if body_bytes else ""
164
+
165
+ # Build headers dict (single value per key for simplicity)
166
+ headers = {k: v for k, v in request.headers.items()
167
+ if k.lower() not in ("host", "connection", "upgrade")}
168
+
169
+ req_id = str(uuid.uuid4())
170
+ msg = {
171
+ "type": "request",
172
+ "id": req_id,
173
+ "method": request.method,
174
+ "path": "/" + subpath,
175
+ "query": request.query_string,
176
+ "headers": headers,
177
+ "body": body_b64,
178
+ }
179
+
180
+ fut: asyncio.Future = asyncio.get_event_loop().create_future()
181
+ pending[req_id] = fut
182
+
183
+ try:
184
+ ws = svc["ws"]
185
+ await ws.send_json(msg)
186
+ resp_data = await asyncio.wait_for(fut, timeout=30)
187
+ except (asyncio.TimeoutError, Exception) as e:
188
+ pending.pop(req_id, None)
189
+ return web.Response(status=504, text=f"Gateway timeout: {e}")
190
+
191
+ # Build response
192
+ status = resp_data.get("status", 502)
193
+ resp_headers = resp_data.get("headers", {})
194
+ resp_body = base64.b64decode(resp_data.get("body", "")) if resp_data.get("body") else b""
195
+
196
+ # Filter hop-by-hop headers
197
+ skip = {"transfer-encoding", "connection", "keep-alive"}
198
+ filtered = {k: v for k, v in resp_headers.items() if k.lower() not in skip}
199
+
200
+ return web.Response(status=status, headers=filtered, body=resp_body)
201
+
202
+
203
+ # ---------------------------------------------------------------------------
204
+ # App setup
205
+ # ---------------------------------------------------------------------------
206
+
207
+ def create_app() -> web.Application:
208
+ app = web.Application()
209
+ app.router.add_get("/", handle_dashboard)
210
+ app.router.add_get("/api/services", handle_api_services)
211
+ app.router.add_get("/ws", handle_ws)
212
+ app.router.add_route("*", "/proxy/{service_id}/{path:.*}", handle_proxy)
213
+ return app
214
+
215
+
216
+ def main():
217
+ parser = argparse.ArgumentParser(description="Kite Server")
218
+ parser.add_argument("--host", default="0.0.0.0", help="Listen address (default: 0.0.0.0)")
219
+ parser.add_argument("--port", type=int, default=8080, help="Listen port (default: 8080)")
220
+ args = parser.parse_args()
221
+
222
+ print(f"Kite server starting on {args.host}:{args.port}")
223
+ web.run_app(create_app(), host=args.host, port=args.port)
224
+
225
+
226
+ if __name__ == "__main__":
227
+ main()
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.4
2
+ Name: kite-strings
3
+ Version: 0.1.0
4
+ Summary: Expose web services on internal servers through a cloud relay with a dashboard and reverse proxy over WebSocket tunnels.
5
+ Author: fuqingxu
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/binary-husky/kite-string
8
+ Requires-Python: >=3.8
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: aiohttp>=3.9
12
+ Dynamic: license-file
13
+
14
+ # Kite - 反向隧道服务注册系统
15
+
16
+ 将内网服务器上的 Web 服务通过云服务器暴露给外部访问。
17
+
18
+ ## 适用场景
19
+
20
+ ```
21
+ ┌─────────┐ WebSocket ┌─────────────┐ HTTP ┌─────────┐
22
+ │ 内网服务器├──────────────►│ 云服务器 X │◄─────────│ 浏览器 │
23
+ │ A/B/C │ │ kite-server │ └─────────┘
24
+ │(kite-client) │ Dashboard │
25
+ └─────────┘ │ + 反向代理 │
26
+ └─────────────┘
27
+ ```
28
+
29
+ - 内网服务器 A/B/C 无法从外部直接访问
30
+ - A/B/C 可以主动连接云服务器 X,反之不行
31
+ - 需要通过 X 访问 A/B/C 上的 Web 服务
32
+
33
+ ## 安装
34
+
35
+ Python 3.8+,唯一依赖 `aiohttp`:
36
+
37
+ ```bash
38
+ pip install aiohttp
39
+ ```
40
+
41
+ ## 使用
42
+
43
+ ### 1. 在云服务器 X 上启动 Server
44
+
45
+ ```bash
46
+ python kite_server.py --port 8080
47
+ ```
48
+
49
+ 可选参数:
50
+
51
+ | 参数 | 默认值 | 说明 |
52
+ |------|--------|------|
53
+ | `--host` | `0.0.0.0` | 监听地址 |
54
+ | `--port` | `8080` | 监听端口 |
55
+
56
+ ### 2. 在内网服务器上启动 Client
57
+
58
+ 假设 A 上有一个运行在 3000 端口的 Web 服务:
59
+
60
+ ```bash
61
+ python kite_client.py --server ws://X:8080/ws --port 3000 --name "my-api"
62
+ ```
63
+
64
+ 可选参数:
65
+
66
+ | 参数 | 必填 | 默认值 | 说明 |
67
+ |------|------|--------|------|
68
+ | `--server` | 是 | - | Server 的 WebSocket 地址 |
69
+ | `--port` | 是 | - | 本地 Web 服务端口 |
70
+ | `--name` | 否 | `主机名:端口` | 服务显示名称 |
71
+
72
+ ### 3. 访问
73
+
74
+ - **Dashboard**:浏览器打开 `http://X:8080`,查看所有已注册服务
75
+ - **代理访问**:点击服务名称,或直接访问 `http://X:8080/proxy/{service_id}/`
76
+
77
+ ## 工作原理
78
+
79
+ 1. Client 通过 WebSocket 连接 Server,注册本地服务信息
80
+ 2. Server 记录服务并在 Dashboard 展示
81
+ 3. 浏览器访问代理路径时,Server 将 HTTP 请求通过 WebSocket 转发给 Client
82
+ 4. Client 向本地服务发起请求,将响应原路返回
83
+ 5. Client 断线后自动重连(指数退避,1s → 30s)
84
+
85
+ ## 示例
86
+
87
+ 在同一台机器上快速体验:
88
+
89
+ ```bash
90
+ # 终端 1:启动 Server
91
+ python kite_server.py --port 8080
92
+
93
+ # 终端 2:启动一个测试 Web 服务
94
+ python -m http.server 9000
95
+
96
+ # 终端 3:启动 Client,将 9000 端口暴露到 Server
97
+ python kite_client.py --server ws://localhost:8080/ws --port 9000 --name "file-server"
98
+
99
+ # 浏览器打开 http://localhost:8080 查看 Dashboard,点击服务即可访问
100
+ ```
101
+
102
+ ## 多服务注册
103
+
104
+ 每个 Web 服务启动一个 Client 实例即可:
105
+
106
+ ```bash
107
+ # Server A
108
+ python kite_client.py --server ws://X:8080/ws --port 3000 --name "api-server"
109
+ python kite_client.py --server ws://X:8080/ws --port 8888 --name "jupyter"
110
+
111
+ # Server B
112
+ python kite_client.py --server ws://X:8080/ws --port 5000 --name "flask-app"
113
+ ```
@@ -0,0 +1,12 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ kite_string/__init__.py
5
+ kite_string/client.py
6
+ kite_string/server.py
7
+ kite_strings.egg-info/PKG-INFO
8
+ kite_strings.egg-info/SOURCES.txt
9
+ kite_strings.egg-info/dependency_links.txt
10
+ kite_strings.egg-info/entry_points.txt
11
+ kite_strings.egg-info/requires.txt
12
+ kite_strings.egg-info/top_level.txt
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ kite-client = kite_string.client:main
3
+ kite-server = kite_string.server:main
@@ -0,0 +1 @@
1
+ aiohttp>=3.9
@@ -0,0 +1 @@
1
+ kite_string
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "kite-strings"
7
+ version = "0.1.0"
8
+ description = "Expose web services on internal servers through a cloud relay with a dashboard and reverse proxy over WebSocket tunnels."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ requires-python = ">=3.8"
12
+ authors = [
13
+ {name = "fuqingxu"},
14
+ ]
15
+ dependencies = [
16
+ "aiohttp>=3.9",
17
+ ]
18
+
19
+ [project.scripts]
20
+ kite-server = "kite_string.server:main"
21
+ kite-client = "kite_string.client:main"
22
+
23
+ [project.urls]
24
+ Homepage = "https://github.com/binary-husky/kite-string"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+