BG5 commited on
Commit
ca0d33d
·
verified ·
1 Parent(s): a4d706b

Upload 7 files

Browse files
Files changed (2) hide show
  1. app.py +181 -90
  2. templates/terminal.html +63 -14
app.py CHANGED
@@ -7,8 +7,13 @@ import signal
7
  import queue
8
  from datetime import datetime
9
  from flask import Flask, render_template, request, jsonify, send_file, abort, redirect, url_for
10
- from flask_socketio import SocketIO, emit
11
  import sys
 
 
 
 
 
12
 
13
  app = Flask(__name__)
14
  app.config['SECRET_KEY'] = 'webshell-secret-key-2024'
@@ -25,21 +30,27 @@ class Terminal:
25
  self.output_queue = queue.Queue()
26
  self.input_queue = queue.Queue()
27
  self.running = False
 
28
 
29
  def spawn(self):
30
  """启动终端进程"""
31
  try:
 
32
  if self.is_windows:
33
- # Windows环境使用cmd
34
  self.process = subprocess.Popen(
35
- 'cmd',
36
  stdin=subprocess.PIPE,
37
  stdout=subprocess.PIPE,
38
  stderr=subprocess.STDOUT,
39
  text=True,
40
- shell=True,
41
- bufsize=0
 
 
 
42
  )
 
43
  else:
44
  # Unix/Linux环境使用bash
45
  import pty
@@ -50,6 +61,7 @@ class Terminal:
50
  else:
51
  # 父进程
52
  self.set_winsize(24, 80)
 
53
 
54
  self.running = True
55
 
@@ -57,41 +69,92 @@ class Terminal:
57
  if self.is_windows:
58
  self._start_output_thread()
59
  self._start_input_thread()
 
 
60
 
 
61
  return True
62
  except Exception as e:
63
- print(f"启动终端失败: {e}")
64
  return False
65
 
66
  def _start_output_thread(self):
67
  """启动输出读取线程(Windows)"""
68
  def read_output():
 
69
  while self.running and self.process and self.process.poll() is None:
70
  try:
71
- line = self.process.stdout.readline()
72
- if line:
73
- self.output_queue.put(line)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  except Exception as e:
75
- print(f"读取输出错误: {e}")
76
  break
77
 
 
 
78
  threading.Thread(target=read_output, daemon=True).start()
79
 
80
  def _start_input_thread(self):
81
  """启动输入处理线程(Windows)"""
82
  def process_input():
 
83
  while self.running and self.process and self.process.poll() is None:
84
  try:
85
  if not self.input_queue.empty():
86
  data = self.input_queue.get(timeout=0.1)
 
87
  if self.process.stdin:
88
  self.process.stdin.write(data)
89
  self.process.stdin.flush()
90
- except:
91
- pass
 
 
 
 
 
 
92
  time.sleep(0.01)
 
93
 
94
  threading.Thread(target=process_input, daemon=True).start()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
  def set_winsize(self, rows, cols):
97
  """设置终端窗口大小"""
@@ -102,52 +165,48 @@ class Terminal:
102
  import fcntl
103
  winsize = struct.pack("HHHH", rows, cols, 0, 0)
104
  fcntl.ioctl(self.fd, termios.TIOCSWINSZ, winsize)
105
- except:
106
- pass
 
107
 
108
  def read(self):
109
  """读取终端输出"""
110
  try:
111
- if self.is_windows:
112
- output = ""
113
- while not self.output_queue.empty():
114
- try:
115
- line = self.output_queue.get_nowait()
116
- output += line
117
- except queue.Empty:
118
- break
119
- return output
120
- else:
121
- # Unix/Linux
122
- if hasattr(self, 'fd'):
123
- try:
124
- import select
125
- ready, _, _ = select.select([self.fd], [], [], 0.1)
126
- if ready:
127
- data = os.read(self.fd, 1024)
128
- return data.decode('utf-8', errors='ignore')
129
- except:
130
- pass
131
- return ''
132
  except Exception as e:
133
- print(f"读取终端输出错误: {e}")
134
  return ''
135
 
136
  def write(self, data):
137
  """写入终端输入"""
138
  try:
 
139
  if self.is_windows:
140
  self.input_queue.put(data)
 
141
  else:
142
  # Unix/Linux
143
  if hasattr(self, 'fd'):
144
  os.write(self.fd, data.encode('utf-8'))
 
145
  except Exception as e:
146
- print(f"写入终端输入错误: {e}")
147
 
148
  def close(self):
149
  """关闭终端"""
150
  try:
 
151
  self.running = False
152
  if self.is_windows:
153
  if self.process:
@@ -162,7 +221,7 @@ class Terminal:
162
  except:
163
  pass
164
  except Exception as e:
165
- print(f"关闭终端错误: {e}")
166
 
167
  @app.route('/')
168
  def index():
@@ -180,31 +239,49 @@ def terminal():
180
  def list_files():
181
  path = request.args.get('path', '.')
182
  try:
183
- # 安全检查:防止路径遍历攻击
184
  abs_path = os.path.abspath(path)
185
- if not abs_path.startswith(os.getcwd()):
186
- # 限制在当前工作目录内
 
187
  abs_path = os.getcwd()
188
 
189
  items = []
190
- for item in os.listdir(abs_path):
191
- item_path = os.path.join(abs_path, item)
192
- try:
193
- stat = os.stat(item_path)
194
- items.append({
195
- 'name': item,
196
- 'type': 'directory' if os.path.isdir(item_path) else 'file',
197
- 'size': stat.st_size,
198
- 'modified': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
199
- 'path': os.path.relpath(item_path, os.getcwd()).replace('\\', '/')
200
- })
201
- except:
202
- continue
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
 
204
  return jsonify({
205
  'success': True,
206
- 'path': os.path.relpath(abs_path, os.getcwd()).replace('\\', '/'),
207
- 'items': sorted(items, key=lambda x: (x['type'] != 'directory', x['name'].lower()))
208
  })
209
  except Exception as e:
210
  return jsonify({'success': False, 'error': str(e)})
@@ -214,10 +291,7 @@ def create_file():
214
  data = request.json
215
  try:
216
  file_path = data['path']
217
- # 安全检查
218
  abs_path = os.path.abspath(file_path)
219
- if not abs_path.startswith(os.getcwd()):
220
- return jsonify({'success': False, 'error': '路径不被允许'})
221
 
222
  if data['type'] == 'directory':
223
  os.makedirs(abs_path, exist_ok=True)
@@ -235,8 +309,6 @@ def delete_file():
235
  try:
236
  file_path = data['path']
237
  abs_path = os.path.abspath(file_path)
238
- if not abs_path.startswith(os.getcwd()):
239
- return jsonify({'success': False, 'error': '路径不被允许'})
240
 
241
  if os.path.isdir(abs_path):
242
  import shutil
@@ -253,9 +325,6 @@ def rename_file():
253
  try:
254
  old_path = os.path.abspath(data['oldPath'])
255
  new_path = os.path.abspath(data['newPath'])
256
-
257
- if not old_path.startswith(os.getcwd()) or not new_path.startswith(os.getcwd()):
258
- return jsonify({'success': False, 'error': '路径不被允许'})
259
 
260
  os.rename(old_path, new_path)
261
  return jsonify({'success': True})
@@ -267,8 +336,6 @@ def read_file():
267
  file_path = request.args.get('path')
268
  try:
269
  abs_path = os.path.abspath(file_path)
270
- if not abs_path.startswith(os.getcwd()):
271
- return jsonify({'success': False, 'error': '路径不被允许'})
272
 
273
  with open(abs_path, 'r', encoding='utf-8') as f:
274
  content = f.read()
@@ -290,8 +357,6 @@ def write_file():
290
  file_path = data['path']
291
  content = data['content']
292
  abs_path = os.path.abspath(file_path)
293
- if not abs_path.startswith(os.getcwd()):
294
- return jsonify({'success': False, 'error': '路径不被允许'})
295
 
296
  with open(abs_path, 'w', encoding='utf-8') as f:
297
  f.write(content)
@@ -304,8 +369,6 @@ def download_file():
304
  file_path = request.args.get('path')
305
  try:
306
  abs_path = os.path.abspath(file_path)
307
- if not abs_path.startswith(os.getcwd()):
308
- abort(403)
309
  return send_file(abs_path, as_attachment=True)
310
  except Exception as e:
311
  abort(404)
@@ -316,8 +379,6 @@ def upload_file():
316
  file = request.files['file']
317
  path = request.form.get('path', '.')
318
  abs_path = os.path.abspath(path)
319
- if not abs_path.startswith(os.getcwd()):
320
- return jsonify({'success': False, 'error': '路径不被允许'})
321
 
322
  file_path = os.path.join(abs_path, file.filename)
323
  file.save(file_path)
@@ -328,63 +389,93 @@ def upload_file():
328
  # WebSocket事件处理
329
  @socketio.on('connect')
330
  def on_connect():
331
- print(f'客户端连接: {request.sid}')
332
 
333
  @socketio.on('disconnect')
334
  def on_disconnect():
335
- print(f'客户端断开: {request.sid}')
 
 
 
 
 
336
  # 清理终端进程
337
- if request.sid in terminals:
338
- terminals[request.sid].close()
339
- del terminals[request.sid]
340
 
341
  @socketio.on('start_terminal')
342
  def on_start_terminal():
343
  session_id = request.sid
 
 
 
 
 
 
344
  terminal = Terminal(session_id)
345
 
346
  if terminal.spawn():
347
  terminals[session_id] = terminal
348
 
349
- # 发送初始提示
 
 
 
 
 
350
  if os.name == 'nt':
351
- socketio.emit('terminal_output', {'data': f'Microsoft Windows [版本 {os.environ.get("OS", "Windows")}]\n'}, room=session_id)
352
- socketio.emit('terminal_output', {'data': f'(c) Microsoft Corporation. 保留所有权利。\n\n'}, room=session_id)
353
- socketio.emit('terminal_output', {'data': f'{os.getcwd()}>'}, room=session_id)
354
  else:
355
- socketio.emit('terminal_output', {'data': f'WebShell Terminal - Linux\n'}, room=session_id)
356
- socketio.emit('terminal_output', {'data': f'Current directory: {os.getcwd()}\n'}, room=session_id)
357
- socketio.emit('terminal_output', {'data': f'$ '}, room=session_id)
358
 
359
  # 启动读取线程
360
  def read_thread():
 
361
  while session_id in terminals and terminals[session_id].running:
362
  try:
363
  terminal_instance = terminals[session_id]
364
  output = terminal_instance.read()
365
  if output:
 
 
366
  socketio.emit('terminal_output', {'data': output}, room=session_id)
367
- time.sleep(0.1)
368
  except Exception as e:
369
- print(f"读取线程错误: {e}")
370
  break
 
371
 
372
  threading.Thread(target=read_thread, daemon=True).start()
373
  emit('terminal_ready')
 
374
  else:
375
  emit('terminal_error', {'error': '无法启动终端'})
 
376
 
377
  @socketio.on('terminal_input')
378
  def on_terminal_input(data):
379
  session_id = request.sid
 
 
 
380
  if session_id in terminals:
381
- command = data['data']
382
- print(f"收到命令: {repr(command)}")
383
- terminals[session_id].write(command)
 
 
 
 
 
 
 
 
384
 
385
  @socketio.on('terminal_resize')
386
  def on_terminal_resize(data):
387
  session_id = request.sid
 
388
  if session_id in terminals:
389
  terminals[session_id].set_winsize(data['rows'], data['cols'])
390
 
@@ -403,5 +494,5 @@ if __name__ == '__main__':
403
  host = os.environ.get('HOST', '0.0.0.0')
404
  debug = os.environ.get('DEBUG', 'False').lower() == 'true'
405
 
406
- print(f"🚀 WebShell 启动在 {host}:{port}")
407
  socketio.run(app, host=host, port=port, debug=debug, allow_unsafe_werkzeug=True)
 
7
  import queue
8
  from datetime import datetime
9
  from flask import Flask, render_template, request, jsonify, send_file, abort, redirect, url_for
10
+ from flask_socketio import SocketIO, emit, join_room, leave_room
11
  import sys
12
+ import logging
13
+
14
+ # 配置日志
15
+ logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')
16
+ logger = logging.getLogger(__name__)
17
 
18
  app = Flask(__name__)
19
  app.config['SECRET_KEY'] = 'webshell-secret-key-2024'
 
30
  self.output_queue = queue.Queue()
31
  self.input_queue = queue.Queue()
32
  self.running = False
33
+ logger.info(f"创建终端实例: {session_id}, Windows: {self.is_windows}")
34
 
35
  def spawn(self):
36
  """启动终端进程"""
37
  try:
38
+ logger.info(f"启动终端进程 {self.session_id}")
39
  if self.is_windows:
40
+ # Windows环境使用cmd,使用简化的参数
41
  self.process = subprocess.Popen(
42
+ ['cmd.exe', '/Q'], # /Q 参数关闭回显
43
  stdin=subprocess.PIPE,
44
  stdout=subprocess.PIPE,
45
  stderr=subprocess.STDOUT,
46
  text=True,
47
+ shell=False,
48
+ bufsize=1, # 行缓冲
49
+ universal_newlines=True,
50
+ cwd=os.getcwd(),
51
+ env=os.environ.copy()
52
  )
53
+ logger.info(f"CMD进程已启动,PID: {self.process.pid}")
54
  else:
55
  # Unix/Linux环境使用bash
56
  import pty
 
61
  else:
62
  # 父进程
63
  self.set_winsize(24, 80)
64
+ logger.info(f"PTY进程已启动,PID: {self.child_pid}")
65
 
66
  self.running = True
67
 
 
69
  if self.is_windows:
70
  self._start_output_thread()
71
  self._start_input_thread()
72
+ else:
73
+ self._start_pty_thread()
74
 
75
+ logger.info(f"终端 {self.session_id} 启动成功")
76
  return True
77
  except Exception as e:
78
+ logger.error(f"启动终端失败: {e}")
79
  return False
80
 
81
  def _start_output_thread(self):
82
  """启动输出读取线程(Windows)"""
83
  def read_output():
84
+ logger.info(f"输出读取线程启动 {self.session_id}")
85
  while self.running and self.process and self.process.poll() is None:
86
  try:
87
+ # 检查进程是否还活着
88
+ if self.process.poll() is not None:
89
+ logger.warning(f"进程已退出: {self.process.poll()}")
90
+ break
91
+
92
+ # 尝试读取一行
93
+ try:
94
+ line = self.process.stdout.readline()
95
+ if line:
96
+ logger.debug(f"读取到输出: {repr(line)}")
97
+ self.output_queue.put(line)
98
+ else:
99
+ # 没有数据时短暂等待
100
+ time.sleep(0.05)
101
+ except Exception as read_err:
102
+ logger.error(f"读取stdout错误: {read_err}")
103
+ time.sleep(0.1)
104
+
105
  except Exception as e:
106
+ logger.error(f"输出线程异常: {e}")
107
  break
108
 
109
+ logger.info(f"输出读取线程结束 {self.session_id}")
110
+
111
  threading.Thread(target=read_output, daemon=True).start()
112
 
113
  def _start_input_thread(self):
114
  """启动输入处理线程(Windows)"""
115
  def process_input():
116
+ logger.info(f"输入处理线程启动 {self.session_id}")
117
  while self.running and self.process and self.process.poll() is None:
118
  try:
119
  if not self.input_queue.empty():
120
  data = self.input_queue.get(timeout=0.1)
121
+ logger.debug(f"写入命令: {repr(data)}")
122
  if self.process.stdin:
123
  self.process.stdin.write(data)
124
  self.process.stdin.flush()
125
+ logger.debug(f"命令已写入并刷新缓冲区")
126
+
127
+ # 立即发送回显
128
+ if not data.endswith('\n'):
129
+ # 如果不是完整命令,发送回显
130
+ self.output_queue.put(data)
131
+ except Exception as e:
132
+ logger.error(f"处理输入错误: {e}")
133
  time.sleep(0.01)
134
+ logger.info(f"输入处理线程结束 {self.session_id}")
135
 
136
  threading.Thread(target=process_input, daemon=True).start()
137
+
138
+ def _start_pty_thread(self):
139
+ """启动PTY线程(Linux)"""
140
+ def pty_thread():
141
+ logger.info(f"PTY线程启动 {self.session_id}")
142
+ while self.running:
143
+ try:
144
+ import select
145
+ ready, _, _ = select.select([self.fd], [], [], 0.1)
146
+ if ready:
147
+ data = os.read(self.fd, 1024)
148
+ if data:
149
+ output = data.decode('utf-8', errors='ignore')
150
+ logger.debug(f"PTY输出: {repr(output)}")
151
+ self.output_queue.put(output)
152
+ except Exception as e:
153
+ logger.error(f"PTY线程错误: {e}")
154
+ break
155
+ logger.info(f"PTY线程结束 {self.session_id}")
156
+
157
+ threading.Thread(target=pty_thread, daemon=True).start()
158
 
159
  def set_winsize(self, rows, cols):
160
  """设置终端窗口大小"""
 
165
  import fcntl
166
  winsize = struct.pack("HHHH", rows, cols, 0, 0)
167
  fcntl.ioctl(self.fd, termios.TIOCSWINSZ, winsize)
168
+ logger.debug(f"设置终端窗口大小: {rows}x{cols}")
169
+ except Exception as e:
170
+ logger.error(f"设置窗口大小失败: {e}")
171
 
172
  def read(self):
173
  """读取终端输出"""
174
  try:
175
+ output = ""
176
+ count = 0
177
+ while not self.output_queue.empty() and count < 10: # 限制一次读取的数量
178
+ try:
179
+ data = self.output_queue.get_nowait()
180
+ output += data
181
+ count += 1
182
+ except queue.Empty:
183
+ break
184
+ if output:
185
+ logger.debug(f"读取到输出 {self.session_id}: {repr(output[:100])}")
186
+ return output
 
 
 
 
 
 
 
 
 
187
  except Exception as e:
188
+ logger.error(f"读取终端输出错误: {e}")
189
  return ''
190
 
191
  def write(self, data):
192
  """写入终端输入"""
193
  try:
194
+ logger.info(f"接收到输入 {self.session_id}: {repr(data)}")
195
  if self.is_windows:
196
  self.input_queue.put(data)
197
+ logger.debug(f"命令已放入输入队列,队列大小: {self.input_queue.qsize()}")
198
  else:
199
  # Unix/Linux
200
  if hasattr(self, 'fd'):
201
  os.write(self.fd, data.encode('utf-8'))
202
+ logger.debug(f"命令已写入PTY")
203
  except Exception as e:
204
+ logger.error(f"写入终端输入错误: {e}")
205
 
206
  def close(self):
207
  """关闭终端"""
208
  try:
209
+ logger.info(f"关闭终端 {self.session_id}")
210
  self.running = False
211
  if self.is_windows:
212
  if self.process:
 
221
  except:
222
  pass
223
  except Exception as e:
224
+ logger.error(f"关闭终端错误: {e}")
225
 
226
  @app.route('/')
227
  def index():
 
239
  def list_files():
240
  path = request.args.get('path', '.')
241
  try:
242
+ # 获取绝对路径
243
  abs_path = os.path.abspath(path)
244
+
245
+ # 确保路径存在
246
+ if not os.path.exists(abs_path):
247
  abs_path = os.getcwd()
248
 
249
  items = []
250
+
251
+ # 添加返回上级目录的选项(除非已经在根目录)
252
+ parent_path = os.path.dirname(abs_path)
253
+ if abs_path != parent_path: # 不在根目录
254
+ items.append({
255
+ 'name': '..',
256
+ 'type': 'directory',
257
+ 'size': 0,
258
+ 'modified': '',
259
+ 'path': parent_path.replace('\\', '/')
260
+ })
261
+
262
+ # 列出当前目录下的文件和文件夹
263
+ try:
264
+ for item in os.listdir(abs_path):
265
+ item_path = os.path.join(abs_path, item)
266
+ try:
267
+ stat = os.stat(item_path)
268
+ items.append({
269
+ 'name': item,
270
+ 'type': 'directory' if os.path.isdir(item_path) else 'file',
271
+ 'size': stat.st_size,
272
+ 'modified': datetime.fromtimestamp(stat.st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
273
+ 'path': item_path.replace('\\', '/')
274
+ })
275
+ except (PermissionError, OSError):
276
+ # 跳过无权限访问的文件
277
+ continue
278
+ except PermissionError:
279
+ return jsonify({'success': False, 'error': '没有权限访问此目录'})
280
 
281
  return jsonify({
282
  'success': True,
283
+ 'path': abs_path.replace('\\', '/'),
284
+ 'items': sorted(items, key=lambda x: (x['name'] != '..', x['type'] != 'directory', x['name'].lower()))
285
  })
286
  except Exception as e:
287
  return jsonify({'success': False, 'error': str(e)})
 
291
  data = request.json
292
  try:
293
  file_path = data['path']
 
294
  abs_path = os.path.abspath(file_path)
 
 
295
 
296
  if data['type'] == 'directory':
297
  os.makedirs(abs_path, exist_ok=True)
 
309
  try:
310
  file_path = data['path']
311
  abs_path = os.path.abspath(file_path)
 
 
312
 
313
  if os.path.isdir(abs_path):
314
  import shutil
 
325
  try:
326
  old_path = os.path.abspath(data['oldPath'])
327
  new_path = os.path.abspath(data['newPath'])
 
 
 
328
 
329
  os.rename(old_path, new_path)
330
  return jsonify({'success': True})
 
336
  file_path = request.args.get('path')
337
  try:
338
  abs_path = os.path.abspath(file_path)
 
 
339
 
340
  with open(abs_path, 'r', encoding='utf-8') as f:
341
  content = f.read()
 
357
  file_path = data['path']
358
  content = data['content']
359
  abs_path = os.path.abspath(file_path)
 
 
360
 
361
  with open(abs_path, 'w', encoding='utf-8') as f:
362
  f.write(content)
 
369
  file_path = request.args.get('path')
370
  try:
371
  abs_path = os.path.abspath(file_path)
 
 
372
  return send_file(abs_path, as_attachment=True)
373
  except Exception as e:
374
  abort(404)
 
379
  file = request.files['file']
380
  path = request.form.get('path', '.')
381
  abs_path = os.path.abspath(path)
 
 
382
 
383
  file_path = os.path.join(abs_path, file.filename)
384
  file.save(file_path)
 
389
  # WebSocket事件处理
390
  @socketio.on('connect')
391
  def on_connect():
392
+ logger.info(f'客户端连接: {request.sid}')
393
 
394
  @socketio.on('disconnect')
395
  def on_disconnect():
396
+ session_id = request.sid
397
+ logger.info(f'客户端断开: {session_id}')
398
+
399
+ # 离开房间
400
+ leave_room(session_id)
401
+
402
  # 清理终端进程
403
+ if session_id in terminals:
404
+ terminals[session_id].close()
405
+ del terminals[session_id]
406
 
407
  @socketio.on('start_terminal')
408
  def on_start_terminal():
409
  session_id = request.sid
410
+ logger.info(f'启动终端请求: {session_id}')
411
+
412
+ # 将客户端加入专用房间
413
+ join_room(session_id)
414
+ logger.info(f'客户端 {session_id} 加入房间')
415
+
416
  terminal = Terminal(session_id)
417
 
418
  if terminal.spawn():
419
  terminals[session_id] = terminal
420
 
421
+ # 立即发送测试消息
422
+ emit('terminal_output', {'data': '=== WebShell Terminal Started ===\n'})
423
+ emit('terminal_output', {'data': f'Session ID: {session_id}\n'})
424
+ emit('terminal_output', {'data': f'Working Directory: {os.getcwd()}\n'})
425
+ emit('terminal_output', {'data': 'Type commands below:\n'})
426
+
427
  if os.name == 'nt':
428
+ emit('terminal_output', {'data': 'C:\\> '})
 
 
429
  else:
430
+ emit('terminal_output', {'data': '$ '})
 
 
431
 
432
  # 启动读取线程
433
  def read_thread():
434
+ logger.info(f'读取线程启动: {session_id}')
435
  while session_id in terminals and terminals[session_id].running:
436
  try:
437
  terminal_instance = terminals[session_id]
438
  output = terminal_instance.read()
439
  if output:
440
+ logger.info(f'发送输出到客户端 {session_id}: {repr(output)}')
441
+ # 发送到特定房间
442
  socketio.emit('terminal_output', {'data': output}, room=session_id)
443
+ time.sleep(0.05) # 减少延迟
444
  except Exception as e:
445
+ logger.error(f"读取线程错误: {e}")
446
  break
447
+ logger.info(f'读取线程结束: {session_id}')
448
 
449
  threading.Thread(target=read_thread, daemon=True).start()
450
  emit('terminal_ready')
451
+ logger.info(f'终端就绪: {session_id}')
452
  else:
453
  emit('terminal_error', {'error': '无法启动终端'})
454
+ logger.error(f'终端启动失败: {session_id}')
455
 
456
  @socketio.on('terminal_input')
457
  def on_terminal_input(data):
458
  session_id = request.sid
459
+ command = data['data']
460
+ logger.info(f"收到终端输入 {session_id}: {repr(command)}")
461
+
462
  if session_id in terminals:
463
+ terminal = terminals[session_id]
464
+ terminal.write(command)
465
+
466
+ # 检查进程状态
467
+ if terminal.is_windows and terminal.process:
468
+ poll_result = terminal.process.poll()
469
+ logger.debug(f"进程状态 {session_id}: {poll_result}, PID: {terminal.process.pid}")
470
+ if poll_result is not None:
471
+ logger.warning(f"进程已终止 {session_id}: 退出码 {poll_result}")
472
+ else:
473
+ logger.warning(f"终端不存在: {session_id}")
474
 
475
  @socketio.on('terminal_resize')
476
  def on_terminal_resize(data):
477
  session_id = request.sid
478
+ logger.debug(f'终端调整大小: {session_id}, {data["rows"]}x{data["cols"]}')
479
  if session_id in terminals:
480
  terminals[session_id].set_winsize(data['rows'], data['cols'])
481
 
 
494
  host = os.environ.get('HOST', '0.0.0.0')
495
  debug = os.environ.get('DEBUG', 'False').lower() == 'true'
496
 
497
+ logger.info(f"🚀 WebShell 启动在 {host}:{port}")
498
  socketio.run(app, host=host, port=port, debug=debug, allow_unsafe_werkzeug=True)
templates/terminal.html CHANGED
@@ -279,31 +279,73 @@
279
 
280
  // 初始化终端连接
281
  function initializeTerminal() {
282
- socket = io();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
283
 
284
  socket.on('connect', function() {
 
285
  updateConnectionStatus('connected');
286
- addOutput('已连接到服务器', '#27ca3f');
 
287
  socket.emit('start_terminal');
288
  });
289
 
290
  socket.on('disconnect', function() {
 
291
  updateConnectionStatus('disconnected');
292
- addOutput('与服务器断开连接', '#ff5f56');
293
  });
294
 
295
  socket.on('terminal_ready', function() {
296
- addOutput('终端已准备就绪', '#6a9955');
 
297
  updatePrompt();
298
  });
299
 
300
  socket.on('terminal_output', function(data) {
301
- addOutput(data.data, '#cccccc', false);
302
- updatePrompt();
 
 
 
 
 
 
 
 
 
 
 
 
303
  });
304
 
305
  socket.on('error', function(error) {
306
- addOutput('错误: ' + error, '#ff5f56');
 
 
 
 
 
 
 
 
 
 
 
 
307
  });
308
  }
309
 
@@ -399,22 +441,29 @@
399
 
400
  // 添加输出
401
  function addOutput(text, color = '#cccccc', addNewline = true) {
 
402
  const output = document.getElementById('terminalOutput');
403
- const div = document.createElement('div');
404
 
405
- if (color) {
406
- div.style.color = color;
 
407
  }
408
 
409
- div.textContent = text;
410
- if (addNewline && !text.endsWith('\n')) {
411
- div.appendChild(document.createElement('br'));
 
 
 
412
  }
413
 
414
- output.appendChild(div);
 
415
 
416
  // 自动滚动到底部
417
  output.scrollTop = output.scrollHeight;
 
 
418
  }
419
 
420
  // 更新连接状态
 
279
 
280
  // 初始化终端连接
281
  function initializeTerminal() {
282
+ console.log('初始化WebSocket连接...');
283
+
284
+ // 获取当前页面的主机和端口
285
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
286
+ const host = window.location.hostname;
287
+ const port = window.location.port || (window.location.protocol === 'https:' ? '443' : '80');
288
+
289
+ console.log('连接参数:', { protocol, host, port });
290
+
291
+ // 创建Socket.IO连接,明确指定端口
292
+ socket = io({
293
+ forceNew: true,
294
+ reconnection: true,
295
+ timeout: 5000
296
+ });
297
 
298
  socket.on('connect', function() {
299
+ console.log('WebSocket已连接, ID:', socket.id);
300
  updateConnectionStatus('connected');
301
+ addOutput('已连接到服务器\n', '#27ca3f');
302
+ console.log('发送start_terminal事件...');
303
  socket.emit('start_terminal');
304
  });
305
 
306
  socket.on('disconnect', function() {
307
+ console.log('WebSocket已断开连接');
308
  updateConnectionStatus('disconnected');
309
+ addOutput('与服务器断开连接\n', '#ff5f56');
310
  });
311
 
312
  socket.on('terminal_ready', function() {
313
+ console.log('收到terminal_ready事件');
314
+ addOutput('终端已准备就绪\n', '#6a9955');
315
  updatePrompt();
316
  });
317
 
318
  socket.on('terminal_output', function(data) {
319
+ console.log('收到terminal_output事件:', data);
320
+ if (data && data.data) {
321
+ // 确保文本正确显示
322
+ console.log('输出内容:', JSON.stringify(data.data));
323
+ addOutput(data.data, '#cccccc', false);
324
+ updatePrompt();
325
+ } else {
326
+ console.error('terminal_output数据格式错误:', data);
327
+ }
328
+ });
329
+
330
+ socket.on('terminal_error', function(error) {
331
+ console.error('收到terminal_error事件:', error);
332
+ addOutput('错误: ' + (error.error || error) + '\n', '#ff5f56');
333
  });
334
 
335
  socket.on('error', function(error) {
336
+ console.error('WebSocket错误:', error);
337
+ addOutput('连接错误: ' + error + '\n', '#ff5f56');
338
+ });
339
+
340
+ socket.on('connect_error', function(error) {
341
+ console.error('连接失败:', error);
342
+ updateConnectionStatus('disconnected');
343
+ addOutput('连接失败: ' + error + '\n', '#ff5f56');
344
+ });
345
+
346
+ // 添加调试事件监听
347
+ socket.onAny((event, ...args) => {
348
+ console.log('收到WebSocket事件:', event, args);
349
  });
350
  }
351
 
 
441
 
442
  // 添加输出
443
  function addOutput(text, color = '#cccccc', addNewline = true) {
444
+ console.log('添加输出:', text, '颜色:', color);
445
  const output = document.getElementById('terminalOutput');
 
446
 
447
+ if (!text) {
448
+ console.warn('尝试添加空文本到终端');
449
+ return;
450
  }
451
 
452
+ // 创建文本节点而不是div,保持原始格式
453
+ const textNode = document.createTextNode(text);
454
+ const span = document.createElement('span');
455
+
456
+ if (color) {
457
+ span.style.color = color;
458
  }
459
 
460
+ span.appendChild(textNode);
461
+ output.appendChild(span);
462
 
463
  // 自动滚动到底部
464
  output.scrollTop = output.scrollHeight;
465
+
466
+ console.log('输出已添加到终端');
467
  }
468
 
469
  // 更新连接状态