zhuhai111 commited on
Commit
810e71a
·
verified ·
1 Parent(s): 9d8c8d0

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +16 -0
  2. config.json +11 -0
  3. ssh_client.py +474 -0
Dockerfile ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM accetto/ubuntu-vnc-xfce-chromium
2
+
3
+ USER root
4
+
5
+ RUN apt-get update \
6
+ && apt-get install -y python3 python3-pip \
7
+ && pip3 install paramiko \
8
+ && apt-get clean \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ COPY ssh_client.py /workspace/ssh_client.py
12
+ COPY config.json /workspace/config.json
13
+
14
+ WORKDIR /workspace
15
+
16
+ CMD bash -c "python3 ssh_client.py -c config.json & exec /dockerstartup/vnc_startup.sh"
config.json ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "server": "www.ip188.net",
3
+ "port": 30001,
4
+ "username": "aauser",
5
+ "password": "aa111111",
6
+ "timeout": 300,
7
+ "remote_host": "127.0.0.1",
8
+ "remote_port": 19775,
9
+ "local_port": 5901,
10
+ "verbose": false
11
+ }
ssh_client.py ADDED
@@ -0,0 +1,474 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+
4
+ import paramiko
5
+ import socket
6
+ import sys
7
+ import time
8
+ import argparse
9
+ import getpass
10
+ import logging
11
+ import select
12
+ import json
13
+ import os
14
+ from threading import Thread
15
+
16
+ # 设置日志
17
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
18
+ logger = logging.getLogger(__name__)
19
+
20
+ class SSHClient:
21
+ def __init__(self, server, port, username, password=None, key_file=None, timeout=30):
22
+ """
23
+ Initialize SSH client
24
+
25
+ Args:
26
+ server (str): SSH server hostname or IP address
27
+ port (int): SSH server port
28
+ username (str): SSH username
29
+ password (str, optional): SSH password
30
+ key_file (str, optional): Path to private key file
31
+ timeout (int, optional): Connection timeout in seconds
32
+ """
33
+ self.server = server
34
+ self.port = port
35
+ self.username = username
36
+ self.password = password
37
+ self.key_file = key_file
38
+ self.client = None
39
+ self.timeout = timeout
40
+ self.transport = None
41
+
42
+ def connect(self):
43
+ """
44
+ Establish connection to SSH server
45
+
46
+ Returns:
47
+ bool: True if connection successful, False otherwise
48
+ """
49
+ try:
50
+ logger.info(f"尝试连接到 {self.server}:{self.port}...")
51
+ self.client = paramiko.SSHClient()
52
+ self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
53
+
54
+ connect_kwargs = {
55
+ 'hostname': self.server,
56
+ 'port': self.port,
57
+ 'username': self.username,
58
+ 'timeout': self.timeout,
59
+ 'allow_agent': False,
60
+ 'look_for_keys': False
61
+ }
62
+
63
+ if self.password:
64
+ connect_kwargs['password'] = self.password
65
+ elif self.key_file:
66
+ connect_kwargs['key_filename'] = self.key_file
67
+
68
+ # 尝试连接
69
+ self.client.connect(**connect_kwargs)
70
+ self.transport = self.client.get_transport()
71
+
72
+ # 设置保活
73
+ if self.transport:
74
+ self.transport.set_keepalive(60) # 每60秒发送保活包
75
+
76
+ logger.info(f"成功连接到 {self.server}:{self.port} 用户名: {self.username}")
77
+ return True
78
+
79
+ except paramiko.AuthenticationException:
80
+ logger.error("认证失败,请检查用户名和密码")
81
+ return False
82
+ except paramiko.SSHException as e:
83
+ logger.error(f"SSH连接错误: {e}")
84
+ return False
85
+ except socket.timeout:
86
+ logger.error(f"连接到 {self.server}:{self.port} 超时。请检查服务器地址和防火墙设置。")
87
+ return False
88
+ except socket.error as e:
89
+ logger.error(f"socket错误: {e}")
90
+ return False
91
+ except Exception as e:
92
+ logger.error(f"连接到SSH服务器时出错: {e}")
93
+ import traceback
94
+ logger.debug(traceback.format_exc())
95
+ return False
96
+
97
+ def setup_port_forward(self, remote_host, remote_port, local_port):
98
+ """
99
+ Set up port forwarding from server to client (remote to local)
100
+
101
+ Args:
102
+ remote_host (str): Remote host to connect to from the SSH server
103
+ remote_port (int): Remote port to connect to
104
+ local_port (int): Local port to forward to
105
+
106
+ Returns:
107
+ bool: True if port forwarding set up successfully, False otherwise
108
+ """
109
+ try:
110
+ # 确保传输层已经准备好
111
+ if not self.transport or not self.transport.is_active():
112
+ logger.error("SSH传输层未激活,无法设置端口转发")
113
+ return False
114
+
115
+ # 使用reverse_forward_tunnel方法来建立从服务器到客户端的转发
116
+ try:
117
+ logger.info(f"尝试请求端口转发: {remote_host}:{remote_port}")
118
+ self.transport.request_port_forward(remote_host, remote_port)
119
+ except paramiko.SSHException as e:
120
+ error_msg = str(e).lower()
121
+ if "forwarding request denied" in error_msg or "addressnotpermitted" in error_msg:
122
+ logger.error(f"端口转发请求被拒绝: {e}")
123
+ logger.info("BvSshServer端口转发问题排查: ")
124
+ logger.info("1. 检查用户权限: 确认SSH用户账户是否有端口转发权限")
125
+ logger.info("2. 尝试使用不同的远程端口: 有些端口可能被禁止转发")
126
+ logger.info("3. 查看服务器日志: 可能有更多关于拒绝原因的信息")
127
+ logger.info("4. 检查是否有其他应用已经占用了该端口")
128
+ logger.info("5. 尝试使用其他绑定地址,如 'localhost' 而不是 '127.0.0.1'")
129
+ return False
130
+ else:
131
+ raise
132
+
133
+ logger.info(f"设置端口转发: {remote_host}:{remote_port} -> localhost:{local_port}")
134
+
135
+ # 创建一个监听线程来处理转发的连接
136
+ class ForwardServer(Thread):
137
+ def __init__(self, transport, remote_host, remote_port, local_port):
138
+ Thread.__init__(self)
139
+ self.transport = transport
140
+ self.remote_host = remote_host
141
+ self.remote_port = remote_port
142
+ self.local_port = local_port
143
+ self.daemon = True
144
+
145
+ def run(self):
146
+ while True:
147
+ try:
148
+ chan = self.transport.accept(1000)
149
+ if chan is None:
150
+ continue
151
+
152
+ # 建立从通道到本地端口的连接
153
+ thr = Thread(target=self.handler, args=(chan,))
154
+ thr.daemon = True
155
+ thr.start()
156
+ except Exception as e:
157
+ if self.transport.is_active():
158
+ logger.error(f"转发通道接收错误: {e}")
159
+ else:
160
+ break
161
+
162
+ def handler(self, chan):
163
+ try:
164
+ sock = socket.socket()
165
+ try:
166
+ sock.connect(('127.0.0.1', self.local_port))
167
+ except ConnectionRefusedError:
168
+ logger.error(f"连接本地端口 {self.local_port} 被拒绝,请确保本地服务正在运行")
169
+ chan.close()
170
+ return
171
+
172
+ logger.info(f"转发连接 {self.remote_host}:{self.remote_port} -> localhost:{self.local_port}")
173
+
174
+ # 双向数据传输
175
+ while True:
176
+ r, w, x = select.select([sock, chan], [], [])
177
+ if sock in r:
178
+ data = sock.recv(1024)
179
+ if len(data) == 0:
180
+ break
181
+ chan.send(data)
182
+ if chan in r:
183
+ data = chan.recv(1024)
184
+ if len(data) == 0:
185
+ break
186
+ sock.send(data)
187
+ except Exception as e:
188
+ logger.error(f"转发处理错误: {e}")
189
+ finally:
190
+ try:
191
+ sock.close()
192
+ chan.close()
193
+ except:
194
+ pass
195
+
196
+ # 启动转发服务器
197
+ forward_server = ForwardServer(self.transport, remote_host, remote_port, local_port)
198
+ forward_server.start()
199
+
200
+ return True
201
+
202
+ except Exception as e:
203
+ logger.error(f"设置端口转发时出错: {e}")
204
+ # 检查是否包含 BvSshServer 特定的错误信息
205
+ error_str = str(e)
206
+ if "AddressNotPermitted" in error_str or "<parameters" in error_str:
207
+ logger.error("检测到 BvSshServer 特有的错误格式")
208
+ logger.info("你需要检查 BvSshServer 的用户配置,确认你的用户有权限进行端口转发")
209
+ logger.info("如果你有访问 BvSshServer 配置的权限,请检查用户配置中的端口转发设置")
210
+ import traceback
211
+ logger.debug(traceback.format_exc())
212
+ return False
213
+
214
+ def close(self):
215
+ """Close the SSH connection"""
216
+ if self.client:
217
+ self.client.close()
218
+ logger.info("SSH连接已关闭")
219
+
220
+ def load_config_from_json(config_file):
221
+ """
222
+ Load SSH configuration from a JSON file
223
+
224
+ Args:
225
+ config_file (str): Path to the JSON configuration file
226
+
227
+ Returns:
228
+ dict: Configuration parameters as a dictionary
229
+ """
230
+ try:
231
+ if not os.path.exists(config_file):
232
+ logger.error(f"配置文件 {config_file} 不存在")
233
+ return None
234
+
235
+ with open(config_file, 'r', encoding='utf-8') as f:
236
+ config = json.load(f)
237
+
238
+ # 验证必要的配置参数
239
+ required_params = ['server', 'username']
240
+ missing_params = [param for param in required_params if param not in config]
241
+
242
+ if missing_params:
243
+ logger.error(f"配置文件缺少必要的参数: {', '.join(missing_params)}")
244
+ return None
245
+
246
+ # 确保端口是整数类型
247
+ if 'port' in config:
248
+ config['port'] = int(config['port'])
249
+ if 'remote_port' in config:
250
+ config['remote_port'] = int(config['remote_port'])
251
+ if 'local_port' in config:
252
+ config['local_port'] = int(config['local_port'])
253
+
254
+ # 设置默认值
255
+ config.setdefault('port', 22)
256
+ config.setdefault('timeout', 30)
257
+ config.setdefault('remote_host', 'localhost')
258
+ config.setdefault('verbose', False)
259
+
260
+ # 检查端口转发设置是否存在
261
+ if 'remote_port' not in config or 'local_port' not in config:
262
+ logger.warning("配置文件缺少端口转发设置 (remote_port 和/或 local_port)")
263
+
264
+ return config
265
+ except json.JSONDecodeError as e:
266
+ logger.error(f"JSON配置文件解析错误: {e}")
267
+ return None
268
+ except Exception as e:
269
+ logger.error(f"无法加载配置文件: {e}")
270
+ import traceback
271
+ logger.debug(traceback.format_exc())
272
+ return None
273
+
274
+ def create_default_config():
275
+ """
276
+ Create a default configuration dictionary
277
+
278
+ Returns:
279
+ dict: Default configuration parameters
280
+ """
281
+ return {
282
+ "server": "ssh.example.com",
283
+ "port": 22,
284
+ "username": "your_username",
285
+ "password": "your_password",
286
+ # 如果使用密钥认证,可以删除password参数并添加下面的配置
287
+ # "key_file": "/path/to/your/private_key.pem",
288
+ "timeout": 30,
289
+ "remote_host": "localhost",
290
+ "remote_port": 8080,
291
+ "local_port": 8080,
292
+ "verbose": False
293
+ }
294
+
295
+ def save_default_config(file_path):
296
+ """
297
+ Save default configuration template to a JSON file
298
+
299
+ Args:
300
+ file_path (str): Path to save the configuration file
301
+
302
+ Returns:
303
+ bool: True if successful, False otherwise
304
+ """
305
+ try:
306
+ with open(file_path, 'w', encoding='utf-8') as f:
307
+ json.dump(create_default_config(), f, indent=4)
308
+ logger.info(f"默认配置模板已保存到 {file_path}")
309
+ return True
310
+ except Exception as e:
311
+ logger.error(f"保存默认配置失败: {e}")
312
+ return False
313
+
314
+ def main():
315
+ # Parse command line arguments
316
+ parser = argparse.ArgumentParser(description='SSH Client with Port Forwarding')
317
+ parser.add_argument('-s', '--server', help='SSH server hostname or IP')
318
+ parser.add_argument('-p', '--port', type=int, help='SSH server port (default: 22)')
319
+ parser.add_argument('-u', '--username', help='SSH username')
320
+ parser.add_argument('-pw', '--password', help='SSH password (will prompt if not provided)')
321
+ parser.add_argument('-k', '--key_file', help='Path to private key file')
322
+ parser.add_argument('-rh', '--remote_host', default='localhost',
323
+ help='Remote host to connect to from SSH server (default: localhost)')
324
+ parser.add_argument('-rp', '--remote_port', type=int,
325
+ help='Remote port to forward from')
326
+ parser.add_argument('-lp', '--local_port', type=int,
327
+ help='Local port to forward to')
328
+ parser.add_argument('-t', '--timeout', type=int, default=30,
329
+ help='Connection timeout in seconds (default: 30)')
330
+ parser.add_argument('-v', '--verbose', action='store_true',
331
+ help='Enable verbose logging')
332
+ # 添加JSON配置文件选项
333
+ parser.add_argument('-c', '--config', help='JSON configuration file path')
334
+ parser.add_argument('--create-config', help='Create default configuration template and save to the specified path')
335
+
336
+ args = parser.parse_args()
337
+
338
+ # 如果指定了创建配置文件选项
339
+ if args.create_config:
340
+ if save_default_config(args.create_config):
341
+ logger.info("已创建默认配置文件模板,请根据需要修改该文件")
342
+ sys.exit(0)
343
+ else:
344
+ sys.exit(1)
345
+
346
+ # 加载配置
347
+ config = {}
348
+
349
+ # 如果指定了配置文件,从配置文件中加载设置
350
+ if args.config:
351
+ config = load_config_from_json(args.config)
352
+ if config is None:
353
+ logger.error("无法加载配置文件,退出程序")
354
+ sys.exit(1)
355
+ logger.info(f"从配置文件加载的配置: server={config['server']}, port={config['port']}, username={config['username']}")
356
+
357
+ # 命令行参数优先级高于配置文件
358
+ if args.server:
359
+ config['server'] = args.server
360
+ if args.port is not None: # 修复:只在明确提供端口时覆盖配置
361
+ config['port'] = args.port
362
+ if args.username:
363
+ config['username'] = args.username
364
+ if args.password:
365
+ config['password'] = args.password
366
+ if args.key_file:
367
+ config['key_file'] = args.key_file
368
+ if args.remote_host:
369
+ config['remote_host'] = args.remote_host
370
+ if args.remote_port:
371
+ config['remote_port'] = args.remote_port
372
+ if args.local_port:
373
+ config['local_port'] = args.local_port
374
+ if args.timeout:
375
+ config['timeout'] = args.timeout
376
+ if args.verbose:
377
+ config['verbose'] = True
378
+
379
+ # 检查必要的参数是否存在
380
+ missing_params = []
381
+ if 'server' not in config:
382
+ missing_params.append('server')
383
+ if 'username' not in config:
384
+ missing_params.append('username')
385
+ if 'remote_port' not in config:
386
+ missing_params.append('remote_port')
387
+ if 'local_port' not in config:
388
+ missing_params.append('local_port')
389
+
390
+ if missing_params:
391
+ logger.error(f"缺少必要的参数: {', '.join(missing_params)}")
392
+ logger.info("请提供这些参数或使用配置文件")
393
+ parser.print_help()
394
+ sys.exit(1)
395
+
396
+ # 再次确认端口是整数
397
+ if 'port' in config:
398
+ config['port'] = int(config['port'])
399
+ if 'remote_port' in config:
400
+ config['remote_port'] = int(config['remote_port'])
401
+ if 'local_port' in config:
402
+ config['local_port'] = int(config['local_port'])
403
+
404
+ logger.info(f"最终使用的配置: server={config['server']}, port={config['port']}, username={config['username']}")
405
+
406
+ # 设置详细日志级别
407
+ if config.get('verbose', False):
408
+ logger.setLevel(logging.DEBUG)
409
+ logging.getLogger('paramiko').setLevel(logging.DEBUG)
410
+
411
+ # 如果没有提供密码和密钥文件,则提示输入密码
412
+ password = config.get('password')
413
+ key_file = config.get('key_file')
414
+ if not password and not key_file:
415
+ password = getpass.getpass('SSH Password: ')
416
+ config['password'] = password
417
+
418
+ # Create SSH client
419
+ ssh_client = SSHClient(
420
+ server=config['server'],
421
+ port=config['port'],
422
+ username=config['username'],
423
+ password=config.get('password'),
424
+ key_file=config.get('key_file'),
425
+ timeout=config.get('timeout', 30)
426
+ )
427
+
428
+ # Connect to SSH server
429
+ if not ssh_client.connect():
430
+ logger.error("无法连接到SSH服务器,退出程序")
431
+ sys.exit(1)
432
+
433
+ try:
434
+ # Set up port forwarding
435
+ if not ssh_client.setup_port_forward(
436
+ config.get('remote_host', 'localhost'),
437
+ config['remote_port'],
438
+ config['local_port']
439
+ ):
440
+ logger.error("无法设置端口转发,退出程序")
441
+ sys.exit(1)
442
+
443
+ logger.info(f"端口转发已建立: {config.get('remote_host', 'localhost')}:{config['remote_port']} -> localhost:{config['local_port']}")
444
+ logger.info("按 Ctrl+C 退出...")
445
+
446
+ # Keep the connection alive with transport keepalives
447
+ while True:
448
+ if not ssh_client.transport or not ssh_client.transport.is_active():
449
+ logger.error("SSH连接已断开,尝试重新连接...")
450
+ if ssh_client.connect():
451
+ if not ssh_client.setup_port_forward(
452
+ config.get('remote_host', 'localhost'),
453
+ config['remote_port'],
454
+ config['local_port']
455
+ ):
456
+ logger.error("无法重新设置端口转发,退出程序")
457
+ sys.exit(1)
458
+ logger.info("连接和端口转发已恢复")
459
+ else:
460
+ logger.error("无法重新连接,退出程序")
461
+ sys.exit(1)
462
+ time.sleep(5)
463
+
464
+ except KeyboardInterrupt:
465
+ logger.info("\n正在退出...")
466
+ except Exception as e:
467
+ logger.error(f"发生错误: {e}")
468
+ import traceback
469
+ logger.debug(traceback.format_exc())
470
+ finally:
471
+ ssh_client.close()
472
+
473
+ if __name__ == "__main__":
474
+ main()