Spaces:
Building
Building
File size: 19,320 Bytes
810e71a |
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 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 |
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import paramiko
import socket
import sys
import time
import argparse
import getpass
import logging
import select
import json
import os
from threading import Thread
# 设置日志
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
class SSHClient:
def __init__(self, server, port, username, password=None, key_file=None, timeout=30):
"""
Initialize SSH client
Args:
server (str): SSH server hostname or IP address
port (int): SSH server port
username (str): SSH username
password (str, optional): SSH password
key_file (str, optional): Path to private key file
timeout (int, optional): Connection timeout in seconds
"""
self.server = server
self.port = port
self.username = username
self.password = password
self.key_file = key_file
self.client = None
self.timeout = timeout
self.transport = None
def connect(self):
"""
Establish connection to SSH server
Returns:
bool: True if connection successful, False otherwise
"""
try:
logger.info(f"尝试连接到 {self.server}:{self.port}...")
self.client = paramiko.SSHClient()
self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
connect_kwargs = {
'hostname': self.server,
'port': self.port,
'username': self.username,
'timeout': self.timeout,
'allow_agent': False,
'look_for_keys': False
}
if self.password:
connect_kwargs['password'] = self.password
elif self.key_file:
connect_kwargs['key_filename'] = self.key_file
# 尝试连接
self.client.connect(**connect_kwargs)
self.transport = self.client.get_transport()
# 设置保活
if self.transport:
self.transport.set_keepalive(60) # 每60秒发送保活包
logger.info(f"成功连接到 {self.server}:{self.port} 用户名: {self.username}")
return True
except paramiko.AuthenticationException:
logger.error("认证失败,请检查用户名和密码")
return False
except paramiko.SSHException as e:
logger.error(f"SSH连接错误: {e}")
return False
except socket.timeout:
logger.error(f"连接到 {self.server}:{self.port} 超时。请检查服务器地址和防火墙设置。")
return False
except socket.error as e:
logger.error(f"socket错误: {e}")
return False
except Exception as e:
logger.error(f"连接到SSH服务器时出错: {e}")
import traceback
logger.debug(traceback.format_exc())
return False
def setup_port_forward(self, remote_host, remote_port, local_port):
"""
Set up port forwarding from server to client (remote to local)
Args:
remote_host (str): Remote host to connect to from the SSH server
remote_port (int): Remote port to connect to
local_port (int): Local port to forward to
Returns:
bool: True if port forwarding set up successfully, False otherwise
"""
try:
# 确保传输层已经准备好
if not self.transport or not self.transport.is_active():
logger.error("SSH传输层未激活,无法设置端口转发")
return False
# 使用reverse_forward_tunnel方法来建立从服务器到客户端的转发
try:
logger.info(f"尝试请求端口转发: {remote_host}:{remote_port}")
self.transport.request_port_forward(remote_host, remote_port)
except paramiko.SSHException as e:
error_msg = str(e).lower()
if "forwarding request denied" in error_msg or "addressnotpermitted" in error_msg:
logger.error(f"端口转发请求被拒绝: {e}")
logger.info("BvSshServer端口转发问题排查: ")
logger.info("1. 检查用户权限: 确认SSH用户账户是否有端口转发权限")
logger.info("2. 尝试使用不同的远程端口: 有些端口可能被禁止转发")
logger.info("3. 查看服务器日志: 可能有更多关于拒绝原因的信息")
logger.info("4. 检查是否有其他应用已经占用了该端口")
logger.info("5. 尝试使用其他绑定地址,如 'localhost' 而不是 '127.0.0.1'")
return False
else:
raise
logger.info(f"设置端口转发: {remote_host}:{remote_port} -> localhost:{local_port}")
# 创建一个监听线程来处理转发的连接
class ForwardServer(Thread):
def __init__(self, transport, remote_host, remote_port, local_port):
Thread.__init__(self)
self.transport = transport
self.remote_host = remote_host
self.remote_port = remote_port
self.local_port = local_port
self.daemon = True
def run(self):
while True:
try:
chan = self.transport.accept(1000)
if chan is None:
continue
# 建立从通道到本地端口的连接
thr = Thread(target=self.handler, args=(chan,))
thr.daemon = True
thr.start()
except Exception as e:
if self.transport.is_active():
logger.error(f"转发通道接收错误: {e}")
else:
break
def handler(self, chan):
try:
sock = socket.socket()
try:
sock.connect(('127.0.0.1', self.local_port))
except ConnectionRefusedError:
logger.error(f"连接本地端口 {self.local_port} 被拒绝,请确保本地服务正在运行")
chan.close()
return
logger.info(f"转发连接 {self.remote_host}:{self.remote_port} -> localhost:{self.local_port}")
# 双向数据传输
while True:
r, w, x = select.select([sock, chan], [], [])
if sock in r:
data = sock.recv(1024)
if len(data) == 0:
break
chan.send(data)
if chan in r:
data = chan.recv(1024)
if len(data) == 0:
break
sock.send(data)
except Exception as e:
logger.error(f"转发处理错误: {e}")
finally:
try:
sock.close()
chan.close()
except:
pass
# 启动转发服务器
forward_server = ForwardServer(self.transport, remote_host, remote_port, local_port)
forward_server.start()
return True
except Exception as e:
logger.error(f"设置端口转发时出错: {e}")
# 检查是否包含 BvSshServer 特定的错误信息
error_str = str(e)
if "AddressNotPermitted" in error_str or "<parameters" in error_str:
logger.error("检测到 BvSshServer 特有的错误格式")
logger.info("你需要检查 BvSshServer 的用户配置,确认你的用户有权限进行端口转发")
logger.info("如果你有访问 BvSshServer 配置的权限,请检查用户配置中的端口转发设置")
import traceback
logger.debug(traceback.format_exc())
return False
def close(self):
"""Close the SSH connection"""
if self.client:
self.client.close()
logger.info("SSH连接已关闭")
def load_config_from_json(config_file):
"""
Load SSH configuration from a JSON file
Args:
config_file (str): Path to the JSON configuration file
Returns:
dict: Configuration parameters as a dictionary
"""
try:
if not os.path.exists(config_file):
logger.error(f"配置文件 {config_file} 不存在")
return None
with open(config_file, 'r', encoding='utf-8') as f:
config = json.load(f)
# 验证必要的配置参数
required_params = ['server', 'username']
missing_params = [param for param in required_params if param not in config]
if missing_params:
logger.error(f"配置文件缺少必要的参数: {', '.join(missing_params)}")
return None
# 确保端口是整数类型
if 'port' in config:
config['port'] = int(config['port'])
if 'remote_port' in config:
config['remote_port'] = int(config['remote_port'])
if 'local_port' in config:
config['local_port'] = int(config['local_port'])
# 设置默认值
config.setdefault('port', 22)
config.setdefault('timeout', 30)
config.setdefault('remote_host', 'localhost')
config.setdefault('verbose', False)
# 检查端口转发设置是否存在
if 'remote_port' not in config or 'local_port' not in config:
logger.warning("配置文件缺少端口转发设置 (remote_port 和/或 local_port)")
return config
except json.JSONDecodeError as e:
logger.error(f"JSON配置文件解析错误: {e}")
return None
except Exception as e:
logger.error(f"无法加载配置文件: {e}")
import traceback
logger.debug(traceback.format_exc())
return None
def create_default_config():
"""
Create a default configuration dictionary
Returns:
dict: Default configuration parameters
"""
return {
"server": "ssh.example.com",
"port": 22,
"username": "your_username",
"password": "your_password",
# 如果使用密钥认证,可以删除password参数并添加下面的配置
# "key_file": "/path/to/your/private_key.pem",
"timeout": 30,
"remote_host": "localhost",
"remote_port": 8080,
"local_port": 8080,
"verbose": False
}
def save_default_config(file_path):
"""
Save default configuration template to a JSON file
Args:
file_path (str): Path to save the configuration file
Returns:
bool: True if successful, False otherwise
"""
try:
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(create_default_config(), f, indent=4)
logger.info(f"默认配置模板已保存到 {file_path}")
return True
except Exception as e:
logger.error(f"保存默认配置失败: {e}")
return False
def main():
# Parse command line arguments
parser = argparse.ArgumentParser(description='SSH Client with Port Forwarding')
parser.add_argument('-s', '--server', help='SSH server hostname or IP')
parser.add_argument('-p', '--port', type=int, help='SSH server port (default: 22)')
parser.add_argument('-u', '--username', help='SSH username')
parser.add_argument('-pw', '--password', help='SSH password (will prompt if not provided)')
parser.add_argument('-k', '--key_file', help='Path to private key file')
parser.add_argument('-rh', '--remote_host', default='localhost',
help='Remote host to connect to from SSH server (default: localhost)')
parser.add_argument('-rp', '--remote_port', type=int,
help='Remote port to forward from')
parser.add_argument('-lp', '--local_port', type=int,
help='Local port to forward to')
parser.add_argument('-t', '--timeout', type=int, default=30,
help='Connection timeout in seconds (default: 30)')
parser.add_argument('-v', '--verbose', action='store_true',
help='Enable verbose logging')
# 添加JSON配置文件选项
parser.add_argument('-c', '--config', help='JSON configuration file path')
parser.add_argument('--create-config', help='Create default configuration template and save to the specified path')
args = parser.parse_args()
# 如果指定了创建配置文件选项
if args.create_config:
if save_default_config(args.create_config):
logger.info("已创建默认配置文件模板,请根据需要修改该文件")
sys.exit(0)
else:
sys.exit(1)
# 加载配置
config = {}
# 如果指定了配置文件,从配置文件中加载设置
if args.config:
config = load_config_from_json(args.config)
if config is None:
logger.error("无法加载配置文件,退出程序")
sys.exit(1)
logger.info(f"从配置文件加载的配置: server={config['server']}, port={config['port']}, username={config['username']}")
# 命令行参数优先级高于配置文件
if args.server:
config['server'] = args.server
if args.port is not None: # 修复:只在明确提供端口时覆盖配置
config['port'] = args.port
if args.username:
config['username'] = args.username
if args.password:
config['password'] = args.password
if args.key_file:
config['key_file'] = args.key_file
if args.remote_host:
config['remote_host'] = args.remote_host
if args.remote_port:
config['remote_port'] = args.remote_port
if args.local_port:
config['local_port'] = args.local_port
if args.timeout:
config['timeout'] = args.timeout
if args.verbose:
config['verbose'] = True
# 检查必要的参数是否存在
missing_params = []
if 'server' not in config:
missing_params.append('server')
if 'username' not in config:
missing_params.append('username')
if 'remote_port' not in config:
missing_params.append('remote_port')
if 'local_port' not in config:
missing_params.append('local_port')
if missing_params:
logger.error(f"缺少必要的参数: {', '.join(missing_params)}")
logger.info("请提供这些参数或使用配置文件")
parser.print_help()
sys.exit(1)
# 再次确认端口是整数
if 'port' in config:
config['port'] = int(config['port'])
if 'remote_port' in config:
config['remote_port'] = int(config['remote_port'])
if 'local_port' in config:
config['local_port'] = int(config['local_port'])
logger.info(f"最终使用的配置: server={config['server']}, port={config['port']}, username={config['username']}")
# 设置详细日志级别
if config.get('verbose', False):
logger.setLevel(logging.DEBUG)
logging.getLogger('paramiko').setLevel(logging.DEBUG)
# 如果没有提供密码和密钥文件,则提示输入密码
password = config.get('password')
key_file = config.get('key_file')
if not password and not key_file:
password = getpass.getpass('SSH Password: ')
config['password'] = password
# Create SSH client
ssh_client = SSHClient(
server=config['server'],
port=config['port'],
username=config['username'],
password=config.get('password'),
key_file=config.get('key_file'),
timeout=config.get('timeout', 30)
)
# Connect to SSH server
if not ssh_client.connect():
logger.error("无法连接到SSH服务器,退出程序")
sys.exit(1)
try:
# Set up port forwarding
if not ssh_client.setup_port_forward(
config.get('remote_host', 'localhost'),
config['remote_port'],
config['local_port']
):
logger.error("无法设置端口转发,退出程序")
sys.exit(1)
logger.info(f"端口转发已建立: {config.get('remote_host', 'localhost')}:{config['remote_port']} -> localhost:{config['local_port']}")
logger.info("按 Ctrl+C 退出...")
# Keep the connection alive with transport keepalives
while True:
if not ssh_client.transport or not ssh_client.transport.is_active():
logger.error("SSH连接已断开,尝试重新连接...")
if ssh_client.connect():
if not ssh_client.setup_port_forward(
config.get('remote_host', 'localhost'),
config['remote_port'],
config['local_port']
):
logger.error("无法重新设置端口转发,退出程序")
sys.exit(1)
logger.info("连接和端口转发已恢复")
else:
logger.error("无法重新连接,退出程序")
sys.exit(1)
time.sleep(5)
except KeyboardInterrupt:
logger.info("\n正在退出...")
except Exception as e:
logger.error(f"发生错误: {e}")
import traceback
logger.debug(traceback.format_exc())
finally:
ssh_client.close()
if __name__ == "__main__":
main() |