local2public 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.
- local2public-0.1.0/.gitignore +11 -0
- local2public-0.1.0/.python-version +1 -0
- local2public-0.1.0/PKG-INFO +6 -0
- local2public-0.1.0/README.md +5 -0
- local2public-0.1.0/app.py +54 -0
- local2public-0.1.0/local2public/__init__.py +0 -0
- local2public-0.1.0/local2public/cli.py +61 -0
- local2public-0.1.0/pyproject.toml +15 -0
- local2public-0.1.0/requirements.txt +3 -0
- local2public-0.1.0/vercel.json +10 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
from fastapi import FastAPI, Request, Response
|
|
2
|
+
from fastapi.responses import HTMLResponse, JSONResponse
|
|
3
|
+
import asyncio
|
|
4
|
+
import uuid
|
|
5
|
+
from fastapi import WebSocket, WebSocketDisconnect
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
app = FastAPI(
|
|
9
|
+
title="Local To Public",
|
|
10
|
+
docs_url='/',
|
|
11
|
+
redoc_url='/docs',
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
import uuid
|
|
16
|
+
|
|
17
|
+
pending_requests = {} # request_id -> {"request": ..., "future": ...}
|
|
18
|
+
|
|
19
|
+
@app.get("/poll/{name}")
|
|
20
|
+
async def poll(name: str):
|
|
21
|
+
requests = {
|
|
22
|
+
rid: {"method": r["method"], "path": r["path"], "body": r["body"]}
|
|
23
|
+
for rid, r in pending_requests.items()
|
|
24
|
+
}
|
|
25
|
+
return JSONResponse(requests)
|
|
26
|
+
|
|
27
|
+
@app.post("/respond/{request_id}")
|
|
28
|
+
async def respond(request_id: str, request: Request):
|
|
29
|
+
data = await request.json()
|
|
30
|
+
if request_id in pending_requests:
|
|
31
|
+
pending_requests[request_id]["future"].set_result(data)
|
|
32
|
+
return JSONResponse({"ok": True})
|
|
33
|
+
|
|
34
|
+
@app.api_route("/join/{name}/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
|
|
35
|
+
async def proxy(name: str, path: str, request: Request):
|
|
36
|
+
request_id = str(uuid.uuid4())
|
|
37
|
+
loop = asyncio.get_event_loop()
|
|
38
|
+
future = loop.create_future()
|
|
39
|
+
|
|
40
|
+
pending_requests[request_id] = {
|
|
41
|
+
"method": request.method,
|
|
42
|
+
"path": f"/{path}",
|
|
43
|
+
"body": (await request.body()).decode(),
|
|
44
|
+
"future": future,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# wait for CLI to respond
|
|
48
|
+
try:
|
|
49
|
+
response = await asyncio.wait_for(future, timeout=30)
|
|
50
|
+
return Response(content=response["body"], status_code=response["status"])
|
|
51
|
+
except asyncio.TimeoutError:
|
|
52
|
+
return JSONResponse({"error": "timeout"}, status_code=504)
|
|
53
|
+
finally:
|
|
54
|
+
pending_requests.pop(request_id, None)
|
|
File without changes
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
from argparse import ArgumentParser
|
|
2
|
+
import asyncio
|
|
3
|
+
import httpx
|
|
4
|
+
|
|
5
|
+
SERVER_URL = "https://l2p-beige.vercel.app" # change to your deployed server
|
|
6
|
+
|
|
7
|
+
def parse_args():
|
|
8
|
+
parser = ArgumentParser(description="ltp - local to public")
|
|
9
|
+
parser.add_argument("-p", "--port", type=int, required=True)
|
|
10
|
+
parser.add_argument("-n", "--name", type=str, required=True)
|
|
11
|
+
parser.add_argument("-v", "--verbose", action="store_true")
|
|
12
|
+
return parser.parse_args()
|
|
13
|
+
|
|
14
|
+
async def tunnel(local_port: int, name: str, verbose: bool):
|
|
15
|
+
print(f"Tunnel open: {SERVER_URL}/join/{name}/")
|
|
16
|
+
print("Ctrl+C to stop")
|
|
17
|
+
|
|
18
|
+
async with httpx.AsyncClient() as client:
|
|
19
|
+
while True:
|
|
20
|
+
try:
|
|
21
|
+
# poll for pending requests
|
|
22
|
+
resp = await client.get(f"{SERVER_URL}/poll/{name}", timeout=5)
|
|
23
|
+
requests = resp.json()
|
|
24
|
+
|
|
25
|
+
for request_id, req in requests.items():
|
|
26
|
+
if verbose:
|
|
27
|
+
print(f"→ {req['method']} {req['path']}")
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
# forward to local server
|
|
31
|
+
local_resp = await client.request(
|
|
32
|
+
method=req["method"],
|
|
33
|
+
url=f"http://localhost:{local_port}{req['path']}",
|
|
34
|
+
content=req["body"],
|
|
35
|
+
timeout=10,
|
|
36
|
+
)
|
|
37
|
+
# send response back to server
|
|
38
|
+
await client.post(f"{SERVER_URL}/respond/{request_id}", json={
|
|
39
|
+
"status": local_resp.status_code,
|
|
40
|
+
"body": local_resp.text,
|
|
41
|
+
})
|
|
42
|
+
except Exception as e:
|
|
43
|
+
await client.post(f"{SERVER_URL}/respond/{request_id}", json={
|
|
44
|
+
"status": 502,
|
|
45
|
+
"body": str(e),
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
except Exception as e:
|
|
49
|
+
print(f"Poll error: {e}")
|
|
50
|
+
|
|
51
|
+
await asyncio.sleep(0.5)
|
|
52
|
+
|
|
53
|
+
def main():
|
|
54
|
+
args = parse_args()
|
|
55
|
+
try:
|
|
56
|
+
asyncio.run(tunnel(args.port, args.name, args.verbose))
|
|
57
|
+
except KeyboardInterrupt:
|
|
58
|
+
print("\nTunnel closed")
|
|
59
|
+
|
|
60
|
+
if __name__ == "__main__":
|
|
61
|
+
main()
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "local2public"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Local to public tunnel"
|
|
9
|
+
requires-python = ">=3.10"
|
|
10
|
+
dependencies = [
|
|
11
|
+
"httpx",
|
|
12
|
+
]
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
ltp = "ltp.cli:main"
|