| FROM python:3.10-slim |
|
|
| WORKDIR /app |
|
|
| RUN apt-get update && apt-get install -y git wget curl vim build-essential bash |
|
|
| RUN pip install --no-cache-dir fastapi uvicorn |
|
|
| RUN python3 -c " |
| content = ''' |
| from fastapi import FastAPI, WebSocket |
| from fastapi.responses import HTMLResponse |
| import asyncio, os, pty, fcntl, termios, struct, json, signal |
|
|
| app = FastAPI() |
|
|
| @app.get(\"/\") |
| async def root(): |
| return HTMLResponse(open(\"index.html\").read()) |
|
|
| @app.websocket(\"/ws\") |
| async def ws_endpoint(websocket: WebSocket): |
| await websocket.accept() |
| master_fd, slave_fd = pty.openpty() |
| env = {**os.environ, \"TERM\": \"xterm-256color\", \"HOME\": \"/root\", \"USER\": \"root\", \"SHELL\": \"/bin/bash\"} |
| proc = await asyncio.create_subprocess_exec( |
| \"/bin/bash\", \"--login\", |
| stdin=slave_fd, stdout=slave_fd, stderr=slave_fd, |
| close_fds=True, env=env |
| ) |
| os.close(slave_fd) |
|
|
| loop = asyncio.get_event_loop() |
| queue = asyncio.Queue() |
|
|
| def on_output(): |
| try: |
| data = os.read(master_fd, 4096) |
| loop.call_soon_threadsafe(queue.put_nowait, data) |
| except OSError: |
| loop.call_soon_threadsafe(queue.put_nowait, None) |
|
|
| loop.add_reader(master_fd, on_output) |
|
|
| async def sender(): |
| while True: |
| data = await queue.get() |
| if data is None: |
| break |
| try: |
| await websocket.send_bytes(data) |
| except: |
| break |
|
|
| sender_task = asyncio.ensure_future(sender()) |
|
|
| try: |
| while True: |
| msg = await websocket.receive_text() |
| try: |
| obj = json.loads(msg) |
| if obj.get(\"type\") == \"resize\": |
| fcntl.ioctl(master_fd, termios.TIOCSWINSZ, |
| struct.pack(\"HHHH\", obj[\"rows\"], obj[\"cols\"], 0, 0)) |
| continue |
| except: |
| pass |
| os.write(master_fd, msg.encode()) |
| except: |
| pass |
| finally: |
| loop.remove_reader(master_fd) |
| sender_task.cancel() |
| try: os.close(master_fd) |
| except: pass |
| try: proc.terminate() |
| except: pass |
| ''' |
| with open(\"app.py\", \"w\") as f: |
| f.write(content) |
| " |
|
|
| RUN python3 -c " |
| content = open('index.html.tpl', 'r').read() if False else '''<!DOCTYPE html> |
| <html> |
| <head> |
| <meta charset=\"UTF-8\"> |
| <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no\"> |
| <title>Terminal</title> |
| <script src=\"https://cdn.jsdelivr.net/npm/xterm@5.3.0/lib/xterm.js\"></script> |
| <script src=\"https://cdn.jsdelivr.net/npm/xterm-addon-fit@0.8.0/lib/xterm-addon-fit.js\"></script> |
| <link rel=\"stylesheet\" href=\"https://cdn.jsdelivr.net/npm/xterm@5.3.0/css/xterm.css\"> |
| <style> |
| |
| body{background:#1a1a2e;display:flex;flex-direction:column;height:100dvh;overflow:hidden} |
| #term{flex:1;overflow:hidden;padding:4px} |
| #toolbar{background:#16213e;padding:6px 8px;display:flex;flex-wrap:wrap;gap:5px;border-top:1px solid #0f3460} |
| .kb{background:#0f3460;color:#e0e0e0;border:1px solid #533483;border-radius:6px;padding:8px 12px;font-size:13px;font-family:monospace;cursor:pointer;min-width:44px;text-align:center;user-select:none;touch-action:manipulation} |
| .kb:active{background:#533483} |
| .kb.on{background:#533483;border-color:#e94560;color:#e94560} |
| </style> |
| </head> |
| <body> |
| <div id=\"term\"></div> |
| <div id=\"toolbar\"> |
| <div style=\"display:flex;gap:5px\"> |
| <button class=\"kb\" id=\"ctrl-btn\" onclick=\"toggleCtrl()\">Ctrl</button> |
| <button class=\"kb\" onclick=\"send(chr(27))\">Esc</button> |
| <button class=\"kb\" onclick=\"send(chr(9))\">Tab</button> |
| </div> |
| <div style=\"display:flex;gap:5px\"> |
| <button class=\"kb\" onclick=\"ctrlKey(chr(99))\">C</button> |
| <button class=\"kb\" onclick=\"ctrlKey(chr(100))\">D</button> |
| <button class=\"kb\" onclick=\"ctrlKey(chr(108))\">L</button> |
| <button class=\"kb\" onclick=\"ctrlKey(chr(122))\">Z</button> |
| <button class=\"kb\" onclick=\"ctrlKey(chr(97))\">A</button> |
| <button class=\"kb\" onclick=\"ctrlKey(chr(101))\">E</button> |
| <button class=\"kb\" onclick=\"ctrlKey(chr(117))\">U</button> |
| <button class=\"kb\" onclick=\"ctrlKey(chr(107))\">K</button> |
| </div> |
| <div style=\"display:flex;gap:5px\"> |
| <button class=\"kb\" onclick=\"send(ESC+chr(91)+chr(65))\">▲</button> |
| <button class=\"kb\" onclick=\"send(ESC+chr(91)+chr(66))\">▼</button> |
| <button class=\"kb\" onclick=\"send(ESC+chr(91)+chr(68))\">◀</button> |
| <button class=\"kb\" onclick=\"send(ESC+chr(91)+chr(67))\">▶</button> |
| </div> |
| </div> |
| <script> |
| const ESC=\"\x1b\"; |
| function chr(n){return String.fromCharCode(n)} |
| const term=new Terminal({cursorBlink:true,theme:{background:\"#1a1a2e\",foreground:\"#e0e0e0\",cursor:\"#e94560\"},fontFamily:\"monospace\",fontSize:13,scrollback:3000}); |
| const fit=new FitAddon.FitAddon(); |
| term.loadAddon(fit); |
| term.open(document.getElementById(\"term\")); |
| fit.fit(); |
| const ws=new WebSocket(\"ws://\"+location.host+\"/ws\"); |
| ws.binaryType=\"arraybuffer\"; |
| ws.onopen=()=>{term.focus();sendResize()}; |
| ws.onmessage=e=>term.write(e.data instanceof ArrayBuffer?new Uint8Array(e.data):e.data); |
| ws.onclose=()=>term.write(\"\\r\\n[disconnected]\\r\\n\"); |
| term.onData(d=>ws.readyState===1&&ws.send(d)); |
| function sendResize(){ws.readyState===1&&ws.send(JSON.stringify({type:\"resize\",cols:term.cols,rows:term.rows}))} |
| new ResizeObserver(()=>{fit.fit();sendResize()}).observe(document.getElementById(\"term\")); |
| let ctrl=false; |
| function toggleCtrl(){ctrl=!ctrl;document.getElementById(\"ctrl-btn\").classList.toggle(\"on\",ctrl)} |
| function send(d){ws.readyState===1&&ws.send(d)} |
| function ctrlKey(k){send(String.fromCharCode(k.charCodeAt(0)-96));if(ctrl){ctrl=false;document.getElementById(\"ctrl-btn\").classList.remove(\"on\")}} |
| </script> |
| </body> |
| </html>''' |
| with open('index.html','w') as f: |
| f.write(content) |
| " |
|
|
| EXPOSE 7860 |
| CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"] |