File size: 6,044 Bytes
349e337
e4c2107
 
 
744c47a
e4c2107
744c47a
e4c2107
744c47a
 
c26cc29
 
744c47a
349e337
c26cc29
349e337
744c47a
c26cc29
744c47a
349e337
744c47a
50bb712
c26cc29
50bb712
744c47a
50bb712
744c47a
 
 
c26cc29
50bb712
 
 
744c47a
50bb712
744c47a
 
 
 
 
 
50bb712
744c47a
50bb712
744c47a
c26cc29
744c47a
 
 
50bb712
744c47a
 
c26cc29
50bb712
744c47a
50bb712
c26cc29
 
 
50bb712
744c47a
 
 
 
50bb712
 
 
 
c26cc29
 
 
744c47a
 
 
 
 
 
 
 
 
 
 
 
 
50bb712
c26cc29
744c47a
 
c26cc29
744c47a
 
 
c26cc29
744c47a
 
 
 
 
 
 
c26cc29
 
 
744c47a
 
 
 
 
 
c26cc29
744c47a
 
 
 
 
 
 
 
 
c26cc29
744c47a
 
 
 
 
c26cc29
 
 
744c47a
 
 
 
50bb712
744c47a
50bb712
744c47a
 
 
 
 
 
 
 
 
 
 
 
c26cc29
 
744c47a
 
 
 
e4c2107
 
c26cc29
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
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>
*{margin:0;padding:0;box-sizing:border-box}
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"]