feat(ssh): 增加SSH超时配置并改进错误处理
- 在配置中添加SSH_CLONE_TIMEOUT、SSH_TEST_TIMEOUT和SSH_CONNECT_TIMEOUT - 替换print为logging记录关键操作和错误 - 改进SSH命令构建方式防止注入 - 添加分支名验证逻辑 - 优化错误消息显示,避免暴露敏感信息
This commit is contained in:
parent
96e4e21692
commit
b030381ad2
|
|
@ -2,6 +2,7 @@
|
||||||
SSH密钥管理API端点
|
SSH密钥管理API端点
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
@ -17,6 +18,7 @@ from app.services.git_ssh_service import SSHKeyService, GitSSHOperations, clear_
|
||||||
from app.core.encryption import encrypt_sensitive_data, decrypt_sensitive_data
|
from app.core.encryption import encrypt_sensitive_data, decrypt_sensitive_data
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
# Schemas
|
# Schemas
|
||||||
|
|
@ -93,7 +95,8 @@ async def generate_ssh_key(
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"生成SSH密钥失败: {str(e)}")
|
logger.error(f"Failed to generate SSH key for user {current_user.id}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="生成SSH密钥失败,请稍后重试")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/", response_model=SSHKeyResponse)
|
@router.get("/", response_model=SSHKeyResponse)
|
||||||
|
|
@ -130,7 +133,8 @@ async def get_ssh_key(
|
||||||
return {"has_key": False}
|
return {"has_key": False}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"获取SSH密钥失败: {str(e)}")
|
logger.error(f"Failed to get SSH key for user {current_user.id}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="获取SSH密钥失败,请稍后重试")
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/")
|
@router.delete("/")
|
||||||
|
|
@ -169,7 +173,8 @@ async def delete_ssh_key(
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"删除SSH密钥失败: {str(e)}")
|
logger.error(f"Failed to delete SSH key for user {current_user.id}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="删除SSH密钥失败,请稍后重试")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/test", response_model=SSHKeyTestResponse)
|
@router.post("/test", response_model=SSHKeyTestResponse)
|
||||||
|
|
@ -207,7 +212,7 @@ async def test_ssh_key(
|
||||||
|
|
||||||
# 验证密钥对是否匹配
|
# 验证密钥对是否匹配
|
||||||
is_valid = SSHKeyService.verify_key_pair(private_key, public_key)
|
is_valid = SSHKeyService.verify_key_pair(private_key, public_key)
|
||||||
print(f"[SSH Test API] Key pair valid: {is_valid}")
|
logger.debug(f"Key pair validation result: {is_valid}")
|
||||||
|
|
||||||
if not is_valid:
|
if not is_valid:
|
||||||
return {
|
return {
|
||||||
|
|
@ -224,7 +229,8 @@ async def test_ssh_key(
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"测试SSH密钥失败: {str(e)}")
|
logger.error(f"Failed to test SSH key for user {current_user.id}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="测试SSH密钥失败,请稍后重试")
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/known-hosts")
|
@router.delete("/known-hosts")
|
||||||
|
|
@ -251,4 +257,5 @@ async def clear_known_hosts_file(
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=500, detail=f"清理失败: {str(e)}")
|
logger.error(f"Failed to clear known_hosts for user {current_user.id}: {e}")
|
||||||
|
raise HTTPException(status_code=500, detail="清理失败,请稍后重试")
|
||||||
|
|
|
||||||
|
|
@ -93,6 +93,9 @@ class Settings(BaseSettings):
|
||||||
|
|
||||||
# SSH配置
|
# SSH配置
|
||||||
SSH_CONFIG_PATH: str = "./data/ssh" # SSH配置目录(存储known_hosts等)
|
SSH_CONFIG_PATH: str = "./data/ssh" # SSH配置目录(存储known_hosts等)
|
||||||
|
SSH_CLONE_TIMEOUT: int = 300 # SSH克隆超时时间(秒)
|
||||||
|
SSH_TEST_TIMEOUT: int = 15 # SSH测试连接超时时间(秒)
|
||||||
|
SSH_CONNECT_TIMEOUT: int = 10 # SSH连接超时时间(秒)
|
||||||
|
|
||||||
# Agent 配置
|
# Agent 配置
|
||||||
AGENT_MAX_ITERATIONS: int = 50 # Agent 最大迭代次数
|
AGENT_MAX_ITERATIONS: int = 50 # Agent 最大迭代次数
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,9 @@ Git SSH服务 - 生成SSH密钥并使用SSH方式访问Git仓库
|
||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import re
|
||||||
|
import shlex
|
||||||
|
import logging
|
||||||
import tempfile
|
import tempfile
|
||||||
import subprocess
|
import subprocess
|
||||||
import shutil
|
import shutil
|
||||||
|
|
@ -15,6 +18,55 @@ from cryptography.hazmat.primitives import serialization
|
||||||
from cryptography.hazmat.primitives.asymmetric import ed25519, rsa
|
from cryptography.hazmat.primitives.asymmetric import ed25519, rsa
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
|
||||||
|
# 配置日志
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def is_valid_branch_name(branch: str) -> bool:
|
||||||
|
"""
|
||||||
|
验证 Git 分支名是否合法
|
||||||
|
|
||||||
|
Git 分支名规则:
|
||||||
|
- 不能以 . 或 - 开头
|
||||||
|
- 不能包含 .., ~, ^, :, ?, *, [, \\, 空格
|
||||||
|
- 不能以 / 结尾
|
||||||
|
- 不能以 .lock 结尾
|
||||||
|
|
||||||
|
Args:
|
||||||
|
branch: 分支名
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否为合法的分支名
|
||||||
|
"""
|
||||||
|
if not branch:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 基本格式检查:只允许字母、数字、-、_、/、.
|
||||||
|
if not re.match(r'^[\w\-/.]+$', branch):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 不能以 . 或 - 开头
|
||||||
|
if branch.startswith('.') or branch.startswith('-'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 不能以 / 结尾
|
||||||
|
if branch.endswith('/'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 不能以 .lock 结尾
|
||||||
|
if branch.endswith('.lock'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 不能包含连续的 ..
|
||||||
|
if '..' in branch:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 不能包含连续的 //
|
||||||
|
if '//' in branch:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
def get_ssh_config_dir() -> str:
|
def get_ssh_config_dir() -> str:
|
||||||
"""
|
"""
|
||||||
|
|
@ -69,10 +121,10 @@ def clear_known_hosts() -> bool:
|
||||||
# 清空文件内容
|
# 清空文件内容
|
||||||
with open(known_hosts_file, 'w') as f:
|
with open(known_hosts_file, 'w') as f:
|
||||||
f.write('')
|
f.write('')
|
||||||
print(f"[SSH] Cleared known_hosts file: {known_hosts_file}")
|
logger.info(f"Cleared known_hosts file: {known_hosts_file}")
|
||||||
return True
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[SSH] Failed to clear known_hosts: {e}")
|
logger.error(f"Failed to clear known_hosts: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -99,7 +151,7 @@ def set_secure_file_permissions(file_path: str):
|
||||||
check=True
|
check=True
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Warning: Failed to set Windows file permissions: {e}")
|
logger.warning(f"Failed to set Windows file permissions: {e}")
|
||||||
# 尝试使用os.chmod作为后备方案
|
# 尝试使用os.chmod作为后备方案
|
||||||
try:
|
try:
|
||||||
os.chmod(file_path, 0o600)
|
os.chmod(file_path, 0o600)
|
||||||
|
|
@ -145,7 +197,7 @@ class SSHKeyService:
|
||||||
return f"SHA256:{fingerprint}"
|
return f"SHA256:{fingerprint}"
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[SSH] Fingerprint calculation error: {e}")
|
logger.error(f"Fingerprint calculation error: {e}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -187,7 +239,7 @@ class SSHKeyService:
|
||||||
backend=default_backend()
|
backend=default_backend()
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[SSH] Failed to load private key: {e}")
|
logger.debug(f"Failed to load private key: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if not private_key_obj:
|
if not private_key_obj:
|
||||||
|
|
@ -207,7 +259,7 @@ class SSHKeyService:
|
||||||
return expected_public == actual_public
|
return expected_public == actual_public
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"[SSH] Key verification error: {e}")
|
logger.error(f"Key verification error: {e}")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|
@ -302,8 +354,15 @@ class GitSSHOperations:
|
||||||
Returns:
|
Returns:
|
||||||
操作结果字典
|
操作结果字典
|
||||||
"""
|
"""
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
temp_dir = None
|
temp_dir = None
|
||||||
try:
|
try:
|
||||||
|
# 验证分支名(如果提供)
|
||||||
|
if branch and not is_valid_branch_name(branch):
|
||||||
|
logger.warning(f"Invalid branch name rejected: {branch}")
|
||||||
|
return {'success': False, 'message': f'无效的分支名: {branch}'}
|
||||||
|
|
||||||
# 创建临时目录存放SSH密钥
|
# 创建临时目录存放SSH密钥
|
||||||
temp_dir = tempfile.mkdtemp(prefix='deepaudit_ssh_')
|
temp_dir = tempfile.mkdtemp(prefix='deepaudit_ssh_')
|
||||||
key_file = os.path.join(temp_dir, 'id_rsa')
|
key_file = os.path.join(temp_dir, 'id_rsa')
|
||||||
|
|
@ -319,19 +378,18 @@ class GitSSHOperations:
|
||||||
# 设置Git SSH命令,只使用DeepAudit生成的SSH密钥
|
# 设置Git SSH命令,只使用DeepAudit生成的SSH密钥
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
|
|
||||||
# 构建SSH命令(只使用DeepAudit密钥)
|
# 构建SSH命令(使用 shlex.quote 转义路径防止命令注入)
|
||||||
ssh_cmd_parts = [
|
ssh_cmd = (
|
||||||
'ssh',
|
f"ssh -i {shlex.quote(key_file)} "
|
||||||
'-i', key_file,
|
f"-o StrictHostKeyChecking=accept-new "
|
||||||
'-o', 'StrictHostKeyChecking=accept-new', # 首次连接时自动接受并保存host key
|
f"-o UserKnownHostsFile={shlex.quote(known_hosts_file)} "
|
||||||
'-o', f'UserKnownHostsFile={known_hosts_file}', # 使用持久化known_hosts文件
|
f"-o PreferredAuthentications=publickey "
|
||||||
'-o', 'PreferredAuthentications=publickey',
|
f"-o IdentitiesOnly=yes"
|
||||||
'-o', 'IdentitiesOnly=yes' # 只使用指定的密钥,不使用系统默认密钥
|
)
|
||||||
]
|
|
||||||
|
|
||||||
env['GIT_SSH_COMMAND'] = ' '.join(ssh_cmd_parts)
|
env['GIT_SSH_COMMAND'] = ssh_cmd
|
||||||
print(f"[Git Clone] Using DeepAudit SSH key: {key_file}")
|
logger.debug(f"Using SSH key file: {key_file}")
|
||||||
print(f"[Git Clone] Using known_hosts file: {known_hosts_file}")
|
logger.debug(f"Using known_hosts file: {known_hosts_file}")
|
||||||
|
|
||||||
# 执行git clone
|
# 执行git clone
|
||||||
cmd = ['git', 'clone', '--depth', '1']
|
cmd = ['git', 'clone', '--depth', '1']
|
||||||
|
|
@ -344,7 +402,7 @@ class GitSSHOperations:
|
||||||
env=env,
|
env=env,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=300
|
timeout=settings.SSH_CLONE_TIMEOUT
|
||||||
)
|
)
|
||||||
|
|
||||||
if result.returncode == 0:
|
if result.returncode == 0:
|
||||||
|
|
@ -354,6 +412,7 @@ class GitSSHOperations:
|
||||||
'path': target_dir
|
'path': target_dir
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
|
logger.error(f"Git clone failed: {result.stderr}")
|
||||||
return {
|
return {
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': '仓库克隆失败',
|
'message': '仓库克隆失败',
|
||||||
|
|
@ -361,8 +420,10 @@ class GitSSHOperations:
|
||||||
}
|
}
|
||||||
|
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
return {'success': False, 'message': '克隆超时(超过5分钟)'}
|
logger.error(f"Git clone timeout after {settings.SSH_CLONE_TIMEOUT}s")
|
||||||
|
return {'success': False, 'message': f'克隆超时(超过{settings.SSH_CLONE_TIMEOUT}秒)'}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"Git clone error: {e}")
|
||||||
return {'success': False, 'message': f'克隆失败: {str(e)}'}
|
return {'success': False, 'message': f'克隆失败: {str(e)}'}
|
||||||
finally:
|
finally:
|
||||||
# 清理临时文件
|
# 清理临时文件
|
||||||
|
|
@ -429,13 +490,13 @@ class GitSSHOperations:
|
||||||
'content': content
|
'content': content
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"读取文件 {rel_path} 失败: {e}")
|
logger.debug(f"读取文件 {rel_path} 失败: {e}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
return files
|
return files
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"获取SSH仓库文件失败: {e}")
|
logger.error(f"获取SSH仓库文件失败: {e}")
|
||||||
raise
|
raise
|
||||||
finally:
|
finally:
|
||||||
# 清理临时克隆目录
|
# 清理临时克隆目录
|
||||||
|
|
@ -454,6 +515,8 @@ class GitSSHOperations:
|
||||||
Returns:
|
Returns:
|
||||||
测试结果字典
|
测试结果字典
|
||||||
"""
|
"""
|
||||||
|
from app.core.config import settings
|
||||||
|
|
||||||
temp_dir = None
|
temp_dir = None
|
||||||
try:
|
try:
|
||||||
# 从URL提取主机
|
# 从URL提取主机
|
||||||
|
|
@ -462,6 +525,11 @@ class GitSSHOperations:
|
||||||
else:
|
else:
|
||||||
return {'success': False, 'message': 'URL格式无效'}
|
return {'success': False, 'message': 'URL格式无效'}
|
||||||
|
|
||||||
|
# 验证主机名格式(防止注入)
|
||||||
|
if not re.match(r'^[\w\-\.]+$', host_part):
|
||||||
|
logger.warning(f"Invalid host name rejected: {host_part}")
|
||||||
|
return {'success': False, 'message': '无效的主机名'}
|
||||||
|
|
||||||
# 创建临时目录存放密钥
|
# 创建临时目录存放密钥
|
||||||
temp_dir = tempfile.mkdtemp(prefix='deepaudit_ssh_test_')
|
temp_dir = tempfile.mkdtemp(prefix='deepaudit_ssh_test_')
|
||||||
key_file = os.path.join(temp_dir, 'id_rsa')
|
key_file = os.path.join(temp_dir, 'id_rsa')
|
||||||
|
|
@ -472,57 +540,53 @@ class GitSSHOperations:
|
||||||
|
|
||||||
# 验证文件是否被创建
|
# 验证文件是否被创建
|
||||||
if not os.path.exists(key_file):
|
if not os.path.exists(key_file):
|
||||||
return {'success': False, 'message': f'私钥文件创建失败: {key_file}'}
|
return {'success': False, 'message': '私钥文件创建失败'}
|
||||||
|
|
||||||
file_size = os.path.getsize(key_file)
|
|
||||||
|
|
||||||
set_secure_file_permissions(key_file)
|
set_secure_file_permissions(key_file)
|
||||||
|
|
||||||
# 使用持久化的known_hosts文件
|
# 使用持久化的known_hosts文件
|
||||||
known_hosts_file = get_known_hosts_file()
|
known_hosts_file = get_known_hosts_file()
|
||||||
|
|
||||||
# 构建SSH命令(只使用DeepAudit密钥)
|
# 构建SSH命令(使用列表形式避免shell注入)
|
||||||
cmd = [
|
cmd = [
|
||||||
'ssh',
|
'ssh',
|
||||||
'-i', key_file,
|
'-i', key_file,
|
||||||
'-o', 'StrictHostKeyChecking=accept-new', # 首次连接时自动接受并保存host key
|
'-o', 'StrictHostKeyChecking=accept-new',
|
||||||
'-o', f'UserKnownHostsFile={known_hosts_file}', # 使用持久化known_hosts文件
|
'-o', f'UserKnownHostsFile={known_hosts_file}',
|
||||||
'-o', 'ConnectTimeout=10',
|
'-o', f'ConnectTimeout={settings.SSH_CONNECT_TIMEOUT}',
|
||||||
'-o', 'PreferredAuthentications=publickey',
|
'-o', 'PreferredAuthentications=publickey',
|
||||||
'-o', 'IdentitiesOnly=yes', # 只使用指定的密钥,不使用系统默认密钥
|
'-o', 'IdentitiesOnly=yes',
|
||||||
'-v', # 详细输出
|
'-v',
|
||||||
'-T', f'git@{host_part}'
|
'-T', f'git@{host_part}'
|
||||||
]
|
]
|
||||||
|
|
||||||
print(f"[SSH Test] Using known_hosts file: {known_hosts_file}")
|
logger.debug(f"Testing SSH connection to: {host_part}")
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
cmd,
|
cmd,
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
timeout=15
|
timeout=settings.SSH_TEST_TIMEOUT
|
||||||
)
|
)
|
||||||
|
|
||||||
# GitHub/GitLab/CodeUp的SSH测试通常返回非0状态码,但会在输出中显示认证成功
|
# GitHub/GitLab/CodeUp的SSH测试通常返回非0状态码,但会在输出中显示认证成功
|
||||||
output = result.stdout + result.stderr
|
output = result.stdout + result.stderr
|
||||||
output_lower = output.lower()
|
output_lower = output.lower()
|
||||||
|
|
||||||
|
|
||||||
# 特别检查Anonymous(表示公钥未添加或未关联用户账户)
|
# 特别检查Anonymous(表示公钥未添加或未关联用户账户)
|
||||||
# 必须在检查成功之前检查,因为Anonymous表示认证技术上成功但没有关联用户
|
|
||||||
if 'anonymous' in output_lower:
|
if 'anonymous' in output_lower:
|
||||||
return {
|
return {
|
||||||
'success': True,
|
'success': True,
|
||||||
'message': 'SSH连接成功,但公钥未关联用户账户',
|
'message': 'SSH连接成功,但公钥未关联用户账户',
|
||||||
'output': f'提示:服务器显示Anonymous,在使用部署密钥时是正常现象。\n请在Git服务的设置中添加SSH公钥。\n\n原始输出:\n{output}'
|
'output': '提示:服务器显示Anonymous,在使用部署密钥时是正常现象。\n请在Git服务的设置中添加SSH公钥。'
|
||||||
}
|
}
|
||||||
|
|
||||||
# 检查是否认证成功
|
# 检查是否认证成功
|
||||||
success_indicators = [
|
success_indicators = [
|
||||||
('successfully authenticated', True), # GitHub
|
('successfully authenticated', True),
|
||||||
('hi ', True), # GitHub: "Hi username!"
|
('hi ', True),
|
||||||
('welcome to gitlab', '@' in output), # GitLab需要有@username
|
('welcome to gitlab', '@' in output),
|
||||||
('welcome to codeup', '@' in output), # CodeUp需要有@username
|
('welcome to codeup', '@' in output),
|
||||||
]
|
]
|
||||||
|
|
||||||
is_success = False
|
is_success = False
|
||||||
|
|
@ -557,16 +621,18 @@ class GitSSHOperations:
|
||||||
}
|
}
|
||||||
|
|
||||||
except subprocess.TimeoutExpired:
|
except subprocess.TimeoutExpired:
|
||||||
|
logger.warning(f"SSH test timeout after {settings.SSH_TEST_TIMEOUT}s")
|
||||||
return {
|
return {
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': 'SSH连接超时(15秒)',
|
'message': f'SSH连接超时({settings.SSH_TEST_TIMEOUT}秒)',
|
||||||
'output': '连接超时,请检查网络或Git服务可用性'
|
'output': '连接超时,请检查网络或Git服务可用性'
|
||||||
}
|
}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
logger.error(f"SSH test error: {e}")
|
||||||
return {
|
return {
|
||||||
'success': False,
|
'success': False,
|
||||||
'message': f'测试失败: {str(e)}',
|
'message': '测试失败,请稍后重试',
|
||||||
'output': str(e)
|
'output': ''
|
||||||
}
|
}
|
||||||
finally:
|
finally:
|
||||||
if temp_dir and os.path.exists(temp_dir):
|
if temp_dir and os.path.exists(temp_dir):
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue