# cloud_tunnel_http.py — serve HTTP from a Cloud box and reach it through a tunnel
# Run: python cloud_tunnel_http.py
import asyncio
import os
import time
from boxlite import (
ApiKeyCredential,
Boxlite,
BoxliteRestOptions,
BoxOptions,
InboundNetworkSpec,
NetworkSpec,
)
GUEST_PORT = 18080
IMAGE = "ghcr.io/boxlite-ai/boxlite-agent-base:v0.1.0"
async def http_get(tunnel, path: str = "/") -> bytes:
"""Send one HTTP request over one tunnel. connect() consumes the tunnel."""
connection = await tunnel.connect()
try:
request = (
f"GET {path} HTTP/1.1\r\n"
"Host: localhost\r\n"
"Connection: close\r\n\r\n"
).encode()
await connection.write(request)
response = bytearray()
while True:
chunk = await connection.read(64 * 1024)
if not chunk: # an empty read means the far side closed the stream
break
response.extend(chunk)
return bytes(response)
finally:
await connection.close()
async def main() -> None:
# export BOXLITE_API_KEY=<YOUR_API_KEY> before running
rt = Boxlite.rest(BoxliteRestOptions(
url=os.environ.get("BOXLITE_REST_URL", "https://api.boxlite.ai"),
credential=ApiKeyCredential(os.environ["BOXLITE_API_KEY"]),
))
box_id = None
try:
box = await rt.create(
BoxOptions(
image=IMAGE,
# Required for a tunnel: inbound is disabled by default,
# and a tunnel against a private box is refused.
network=NetworkSpec(inbound=InboundNetworkSpec(mode="enabled")),
),
name=f"tunnel-http-{int(time.time())}",
)
box_id = box.id
await box.start()
# Bind 0.0.0.0: the tunnel arrives on the box's network interface,
# so a server on 127.0.0.1 inside the box cannot be reached.
launch = await box.exec("sh", args=[
"-c",
f"nohup python3 -m http.server {GUEST_PORT} --bind 0.0.0.0 "
"> /tmp/server.log 2>&1 & echo launched",
])
await launch.wait()
# uri() is synchronous and does not consume the tunnel.
tunnel = await box.network.tunnel(GUEST_PORT)
print(f"tunnel URL: {tunnel.uri()}")
# The server needs a moment. Each attempt needs its own tunnel.
response = b""
for attempt in range(20):
try:
response = await http_get(tunnel)
if response.startswith(b"HTTP/1."):
break
except Exception as exc:
print(f"attempt {attempt + 1}: {type(exc).__name__}: {exc}")
await asyncio.sleep(1)
tunnel = await box.network.tunnel(GUEST_PORT) # the previous one is spent
if response.startswith(b"HTTP/1."):
print(response.split(b"\r\n", 1)[0].decode())
else:
log = await box.exec("cat", args=["/tmp/server.log"])
text = ""
async for line in log.stdout():
text += line
await log.wait()
print(f"no HTTP response. server log:\n{text}")
except Exception as exc:
print(f"tunnel demo failed: {type(exc).__name__}: {exc}")
finally:
# Teardown on the runtime, in finally, so a failure above cannot leave a box running.
if box_id:
try:
await rt.remove(box_id, force=True)
except Exception as exc:
print(f"remove failed: {exc}")
if __name__ == "__main__":
asyncio.run(main())