fix: harden driver manager uploads

This commit is contained in:
Codex
2026-06-28 22:12:20 +08:00
parent 88a3d3468c
commit 41eb8b8a6e
4 changed files with 196 additions and 127 deletions

View File

@@ -47,6 +47,8 @@ chmod +x setup_cups.sh
./setup_cups.sh --driver-manager-auth ./setup_cups.sh --driver-manager-auth
``` ```
驱动管理器默认最大上传 `512MB`,单个安装命令默认超时 `900` 秒。需要调整时可在服务环境变量中设置 `DRIVER_MANAGER_MAX_UPLOAD_MB``DRIVER_MANAGER_COMMAND_TIMEOUT`
## 驱动选项 ## 驱动选项
安装时可选择以下驱动: 安装时可选择以下驱动:

View File

@@ -14,6 +14,8 @@ RestartSec=5
Environment=DRIVER_MANAGER_USERNAME=admin Environment=DRIVER_MANAGER_USERNAME=admin
Environment=DRIVER_MANAGER_PASSWORD=admin Environment=DRIVER_MANAGER_PASSWORD=admin
Environment=DRIVER_MANAGER_ALLOW_PRIVATE_ONLY=1 Environment=DRIVER_MANAGER_ALLOW_PRIVATE_ONLY=1
Environment=DRIVER_MANAGER_MAX_UPLOAD_MB=512
Environment=DRIVER_MANAGER_COMMAND_TIMEOUT=900
# 安全设置 # 安全设置
NoNewPrivileges=false NoNewPrivileges=false

View File

@@ -12,7 +12,9 @@ import tempfile
import shutil import shutil
import tarfile import tarfile
import zipfile import zipfile
import gzip
import ipaddress import ipaddress
import uuid
from pathlib import Path from pathlib import Path
from functools import wraps from functools import wraps
@@ -22,9 +24,17 @@ from werkzeug.utils import secure_filename
app = Flask(__name__) app = Flask(__name__)
app.secret_key = os.urandom(24) app.secret_key = os.urandom(24)
def env_int(name, default):
try:
return max(1, int(os.environ.get(name, default)))
except ValueError:
return default
# 配置 # 配置
UPLOAD_FOLDER = '/tmp/cups-drivers' UPLOAD_FOLDER = '/tmp/cups-drivers'
MAX_CONTENT_LENGTH = 100 * 1024 * 1024 # 100MB MAX_UPLOAD_MB = env_int('DRIVER_MANAGER_MAX_UPLOAD_MB', 512)
MAX_CONTENT_LENGTH = MAX_UPLOAD_MB * 1024 * 1024
COMMAND_TIMEOUT = env_int('DRIVER_MANAGER_COMMAND_TIMEOUT', 900)
ALLOWED_EXTENSIONS = {'deb', 'ppd', 'gz', 'tar', 'tgz', 'zip', 'rpm', 'sh', 'run'} ALLOWED_EXTENSIONS = {'deb', 'ppd', 'gz', 'tar', 'tgz', 'zip', 'rpm', 'sh', 'run'}
# 管理员凭据(可通过环境变量设置) # 管理员凭据(可通过环境变量设置)
@@ -104,11 +114,11 @@ def allowed_file(filename):
"""检查文件类型是否允许""" """检查文件类型是否允许"""
if '.' not in filename: if '.' not in filename:
return False return False
file_type = get_file_type(filename)
if file_type == 'unknown':
return False
ext = filename.rsplit('.', 1)[1].lower() ext = filename.rsplit('.', 1)[1].lower()
# 处理 .tar.gz 情况 return ext in ALLOWED_EXTENSIONS or file_type in ('tar.gz', 'ppd')
if filename.endswith('.tar.gz'):
return True
return ext in ALLOWED_EXTENSIONS
def get_file_type(filename): def get_file_type(filename):
"""获取文件类型""" """获取文件类型"""
@@ -130,13 +140,47 @@ def get_file_type(filename):
else: else:
return 'unknown' return 'unknown'
def run_command(cmd, shell=False): def upload_extension(filename):
"""保留真实扩展名,避免中文文件名被 secure_filename 清空后丢失类型。"""
filename_lower = filename.lower()
for ext in ('.tar.gz', '.ppd.gz', '.tgz', '.deb', '.ppd', '.tar', '.zip', '.rpm', '.sh', '.run'):
if filename_lower.endswith(ext):
return ext
return Path(filename_lower).suffix
def safe_upload_filename(filename):
safe_name = secure_filename(filename)
ext = upload_extension(filename)
if not safe_name:
safe_name = f'driver{ext}'
elif ext and safe_name.lower() == ext.lstrip('.'):
safe_name = f'driver{ext}'
elif ext and not safe_name.lower().endswith(ext):
safe_name = f'{Path(safe_name).stem or "driver"}{ext}'
return f'{uuid.uuid4().hex}_{safe_name}'
def find_files_by_suffix(root_dir, suffixes):
suffixes = tuple(s.lower() for s in suffixes)
matches = []
for path in Path(root_dir).rglob('*'):
if path.is_file() and path.name.lower().endswith(suffixes):
matches.append(path)
return sorted(matches, key=lambda p: str(p).lower())
def run_command(cmd, shell=False, cwd=None, input_text=None):
"""执行命令并返回结果""" """执行命令并返回结果"""
try: try:
if shell: if shell:
result = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=300) result = subprocess.run(
cmd, shell=True, capture_output=True, text=True,
timeout=COMMAND_TIMEOUT, cwd=cwd, input=input_text,
encoding='utf-8', errors='replace'
)
else: else:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300) result = subprocess.run(
cmd, capture_output=True, text=True, timeout=COMMAND_TIMEOUT,
cwd=cwd, input=input_text, encoding='utf-8', errors='replace'
)
return { return {
'success': result.returncode == 0, 'success': result.returncode == 0,
'stdout': result.stdout, 'stdout': result.stdout,
@@ -144,7 +188,7 @@ def run_command(cmd, shell=False):
'returncode': result.returncode 'returncode': result.returncode
} }
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
return {'success': False, 'stdout': '', 'stderr': '命令执行超时', 'returncode': -1} return {'success': False, 'stdout': '', 'stderr': f'命令执行超时>{COMMAND_TIMEOUT}秒)', 'returncode': -1}
except Exception as e: except Exception as e:
return {'success': False, 'stdout': '', 'stderr': str(e), 'returncode': -1} return {'success': False, 'stdout': '', 'stderr': str(e), 'returncode': -1}
@@ -152,14 +196,23 @@ def install_deb(filepath):
"""安装 .deb 包""" """安装 .deb 包"""
results = [] results = []
# 先尝试直接安装
result = run_command(['dpkg', '-i', filepath]) result = run_command(['dpkg', '-i', filepath])
results.append(('dpkg -i', result)) if result['success']:
results.append(('安装 DEB 包', result))
return results
# 修复依赖 # dpkg 因依赖缺失失败是常见情况,继续修复依赖后重试,不把初次失败计为最终失败。
if not result['success']: results.append(('初次安装 DEB 包', {
'success': True,
'stdout': result['stdout'],
'stderr': '初次 dpkg 安装未完成,正在尝试自动修复依赖后重试。\n' + result['stderr'],
'returncode': 0
}))
fix_result = run_command(['apt-get', 'install', '-f', '-y']) fix_result = run_command(['apt-get', 'install', '-f', '-y'])
results.append(('apt-get install -f', fix_result)) results.append(('修复 DEB 依赖', fix_result))
if fix_result['success']:
retry_result = run_command(['dpkg', '-i', filepath])
results.append(('重新安装 DEB 包', retry_result))
return results return results
@@ -177,21 +230,27 @@ def install_ppd(filepath):
for ppd_dir in ppd_dirs: for ppd_dir in ppd_dirs:
os.makedirs(ppd_dir, exist_ok=True) os.makedirs(ppd_dir, exist_ok=True)
# 复制PPD文件
filename = os.path.basename(filepath) filename = os.path.basename(filepath)
dest = os.path.join(ppd_dirs[0], filename) is_gzipped = filename.lower().endswith('.ppd.gz')
dest_filename = filename[:-3] if is_gzipped else filename
for ppd_dir in ppd_dirs:
dest = os.path.join(ppd_dir, dest_filename)
try: try:
if is_gzipped:
with gzip.open(filepath, 'rb') as src, open(dest, 'wb') as dst:
shutil.copyfileobj(src, dst)
else:
shutil.copy2(filepath, dest) shutil.copy2(filepath, dest)
os.chmod(dest, 0o644) os.chmod(dest, 0o644)
results.append(('复制PPD文件', { results.append((f'安装 PPD 到 {ppd_dir}', {
'success': True, 'success': True,
'stdout': f'复制{dest}', 'stdout': f'安装{dest}',
'stderr': '', 'stderr': '',
'returncode': 0 'returncode': 0
})) }))
except Exception as e: except Exception as e:
results.append(('复制PPD文件', { results.append((f'安装 PPD 到 {ppd_dir}', {
'success': False, 'success': False,
'stdout': '', 'stdout': '',
'stderr': str(e), 'stderr': str(e),
@@ -200,16 +259,64 @@ def install_ppd(filepath):
return results return results
def install_tar_gz(filepath): def install_extracted_dir(extract_dir):
"""安装 .tar.gz 包""" """从已解压目录中自动寻找可安装的驱动文件。"""
results = [] results = []
root_path = Path(extract_dir)
# 创建临时解压目录 deb_files = find_files_by_suffix(extract_dir, ('.deb',))
if deb_files:
for deb_file in deb_files:
results.extend(install_deb(str(deb_file)))
return results
rpm_files = find_files_by_suffix(extract_dir, ('.rpm',))
if rpm_files:
for rpm_file in rpm_files:
results.extend(install_rpm(str(rpm_file)))
return results
install_script_names = {'install.sh', 'setup.sh', 'install', 'setup'}
for path in root_path.rglob('*'):
if path.is_file() and path.name.lower() in install_script_names:
os.chmod(path, 0o755)
if path.name.lower().endswith('.sh'):
result = run_command(['/bin/sh', str(path)], cwd=str(path.parent))
else:
result = run_command([str(path)], cwd=str(path.parent))
results.append(('执行安装脚本', result))
return results
for path in root_path.rglob('*'):
if path.is_file() and path.name.lower() == 'makefile':
make_result = run_command(['make', '-C', str(path.parent)])
results.append(('make', make_result))
if make_result['success']:
install_result = run_command(['make', '-C', str(path.parent), 'install'])
results.append(('make install', install_result))
return results
ppd_files = find_files_by_suffix(extract_dir, ('.ppd', '.ppd.gz'))
if ppd_files:
for ppd_file in ppd_files:
results.extend(install_ppd(str(ppd_file)))
return results
results.append(('查找安装方式', {
'success': False,
'stdout': '',
'stderr': '未找到 DEB、RPM、安装脚本、Makefile 或 PPD 文件',
'returncode': 1
}))
return results
def install_tar_gz(filepath):
"""安装 .tar/.tar.gz/.tgz 包"""
results = []
extract_dir = tempfile.mkdtemp(prefix='driver_') extract_dir = tempfile.mkdtemp(prefix='driver_')
try: try:
# 解压 with tarfile.open(filepath, 'r:*') as tar:
with tarfile.open(filepath, 'r:gz') as tar:
safe_extract_tar(tar, extract_dir) safe_extract_tar(tar, extract_dir)
results.append(('解压文件', { results.append(('解压文件', {
'success': True, 'success': True,
@@ -217,48 +324,7 @@ def install_tar_gz(filepath):
'stderr': '', 'stderr': '',
'returncode': 0 'returncode': 0
})) }))
results.extend(install_extracted_dir(extract_dir))
# 查找安装脚本
install_scripts = ['install.sh', 'setup.sh', 'install', 'setup']
found_script = None
for root, dirs, files in os.walk(extract_dir):
for script in install_scripts:
if script in files:
found_script = os.path.join(root, script)
break
if found_script:
break
if found_script:
os.chmod(found_script, 0o755)
result = run_command(found_script, shell=True)
results.append(('执行安装脚本', result))
else:
# 查找 Makefile
for root, dirs, files in os.walk(extract_dir):
if 'Makefile' in files:
make_result = run_command(['make', '-C', root])
results.append(('make', make_result))
if make_result['success']:
install_result = run_command(['make', '-C', root, 'install'])
results.append(('make install', install_result))
break
else:
# 查找 PPD 文件
ppd_files = list(Path(extract_dir).rglob('*.ppd'))
if ppd_files:
for ppd_file in ppd_files:
ppd_results = install_ppd(str(ppd_file))
results.extend(ppd_results)
else:
results.append(('查找安装方式', {
'success': False,
'stdout': '',
'stderr': '未找到安装脚本、Makefile或PPD文件',
'returncode': 1
}))
except Exception as e: except Exception as e:
results.append(('解压文件', { results.append(('解压文件', {
'success': False, 'success': False,
@@ -289,45 +355,7 @@ def install_zip(filepath):
'stderr': '', 'stderr': '',
'returncode': 0 'returncode': 0
})) }))
results.extend(install_extracted_dir(extract_dir))
# 查找 deb 文件
deb_files = list(Path(extract_dir).rglob('*.deb'))
if deb_files:
for deb_file in deb_files:
deb_results = install_deb(str(deb_file))
results.extend(deb_results)
return results
# 查找安装脚本
install_scripts = ['install.sh', 'setup.sh', 'install', 'setup']
found_script = None
for root, dirs, files in os.walk(extract_dir):
for script in install_scripts:
if script in files:
found_script = os.path.join(root, script)
break
if found_script:
break
if found_script:
os.chmod(found_script, 0o755)
result = run_command(found_script, shell=True)
results.append(('执行安装脚本', result))
else:
# 查找 PPD 文件
ppd_files = list(Path(extract_dir).rglob('*.ppd'))
if ppd_files:
for ppd_file in ppd_files:
ppd_results = install_ppd(str(ppd_file))
results.extend(ppd_results)
else:
results.append(('查找安装方式', {
'success': False,
'stdout': '',
'stderr': '未找到deb包、安装脚本或PPD文件',
'returncode': 1
}))
except Exception as e: except Exception as e:
results.append(('解压文件', { results.append(('解压文件', {
@@ -357,7 +385,7 @@ def install_rpm(filepath):
# 使用 alien 转换 # 使用 alien 转换
work_dir = os.path.dirname(filepath) work_dir = os.path.dirname(filepath)
convert_result = run_command(f'cd {work_dir} && alien -d {filepath}', shell=True) convert_result = run_command(['alien', '-d', filepath], cwd=work_dir)
results.append(('转换RPM为DEB', convert_result)) results.append(('转换RPM为DEB', convert_result))
if convert_result['success']: if convert_result['success']:
@@ -381,11 +409,14 @@ def install_script(filepath):
if 'hplip' in filename and 'plugin' in filename: if 'hplip' in filename and 'plugin' in filename:
# 直接执行 .run 文件,使用 yes 自动确认交互式提示 # 直接执行 .run 文件,使用 yes 自动确认交互式提示
# 注意hp-plugin 命令需要 .asc 签名文件,比较麻烦,所以直接执行 # 注意hp-plugin 命令需要 .asc 签名文件,比较麻烦,所以直接执行
result = run_command(f'yes | sh {filepath}', shell=True) result = run_command(['/bin/sh', filepath], input_text='y\n' * 20)
results.append(('执行 HP 插件安装脚本', result)) results.append(('执行 HP 插件安装脚本', result))
else: else:
# 普通脚本直接执行 # 普通脚本直接执行,避免文件名包含空格时 shell 拼接失败。
result = run_command(filepath, shell=True) if filename.endswith('.sh'):
result = run_command(['/bin/sh', filepath])
else:
result = run_command([filepath])
results.append(('执行安装脚本', result)) results.append(('执行安装脚本', result))
return results return results
@@ -545,6 +576,11 @@ HTML_TEMPLATE = '''
margin: 2px; margin: 2px;
display: inline-block; display: inline-block;
} }
.text-muted {
color: #666;
font-size: 13px;
margin-top: 8px;
}
.alert { .alert {
padding: 15px; padding: 15px;
border-radius: 5px; border-radius: 5px;
@@ -686,11 +722,16 @@ HTML_TEMPLATE = '''
支持的格式: 支持的格式:
<span>.deb</span> <span>.deb</span>
<span>.ppd</span> <span>.ppd</span>
<span>.ppd.gz</span>
<span>.tar</span>
<span>.tgz</span>
<span>.tar.gz</span> <span>.tar.gz</span>
<span>.zip</span> <span>.zip</span>
<span>.rpm</span> <span>.rpm</span>
<span>.sh</span> <span>.sh</span>
<span>.run</span>
</div> </div>
<p class="text-muted">最大上传:{{ max_upload_mb }} MB。压缩包内可自动识别 DEB、RPM、PPD、PPD.GZ、安装脚本和 Makefile。</p>
<br> <br>
<button type="submit" class="btn" id="uploadBtn" disabled>开始安装</button> <button type="submit" class="btn" id="uploadBtn" disabled>开始安装</button>
</form> </form>
@@ -937,7 +978,17 @@ DRIVERS_TEMPLATE = '''
@app.route('/') @app.route('/')
@requires_auth @requires_auth
def index(): def index():
return render_template_string(HTML_TEMPLATE, results=None) return render_template_string(HTML_TEMPLATE, results=None, max_upload_mb=MAX_UPLOAD_MB)
@app.errorhandler(413)
def upload_too_large(error):
if request.path.startswith('/api/'):
return jsonify({
'success': False,
'error': f'文件过大,当前最大允许上传 {MAX_UPLOAD_MB} MB'
}), 413
flash(f'文件过大,当前最大允许上传 {MAX_UPLOAD_MB} MB', 'danger')
return redirect(url_for('index'))
@app.route('/upload', methods=['POST']) @app.route('/upload', methods=['POST'])
@requires_auth @requires_auth
@@ -957,7 +1008,7 @@ def upload_file():
return redirect(url_for('index')) return redirect(url_for('index'))
# 保存文件 # 保存文件
filename = secure_filename(file.filename) filename = safe_upload_filename(file.filename)
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath) file.save(filepath)
@@ -980,7 +1031,7 @@ def upload_file():
else: else:
flash('驱动安装过程中出现错误,请查看详细信息', 'danger') flash('驱动安装过程中出现错误,请查看详细信息', 'danger')
return render_template_string(HTML_TEMPLATE, results=results) return render_template_string(HTML_TEMPLATE, results=results, max_upload_mb=MAX_UPLOAD_MB)
@app.route('/drivers') @app.route('/drivers')
@requires_auth @requires_auth
@@ -1030,7 +1081,7 @@ def api_install():
return jsonify({'success': False, 'error': '不支持的文件类型'}) return jsonify({'success': False, 'error': '不支持的文件类型'})
# 保存文件 # 保存文件
filename = secure_filename(file.filename) filename = safe_upload_filename(file.filename)
filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename) filepath = os.path.join(app.config['UPLOAD_FOLDER'], filename)
file.save(filepath) file.save(filepath)

View File

@@ -24,6 +24,8 @@ CUPS_ALLOWED_NET="${CUPS_ALLOWED_NET:-@LOCAL}"
DRIVER_MANAGER_HOST="${DRIVER_MANAGER_HOST:-0.0.0.0}" DRIVER_MANAGER_HOST="${DRIVER_MANAGER_HOST:-0.0.0.0}"
DRIVER_MANAGER_USERNAME="${DRIVER_MANAGER_USERNAME:-admin}" DRIVER_MANAGER_USERNAME="${DRIVER_MANAGER_USERNAME:-admin}"
DRIVER_MANAGER_PRIVATE_ONLY="${DRIVER_MANAGER_PRIVATE_ONLY:-1}" DRIVER_MANAGER_PRIVATE_ONLY="${DRIVER_MANAGER_PRIVATE_ONLY:-1}"
DRIVER_MANAGER_MAX_UPLOAD_MB="${DRIVER_MANAGER_MAX_UPLOAD_MB:-512}"
DRIVER_MANAGER_COMMAND_TIMEOUT="${DRIVER_MANAGER_COMMAND_TIMEOUT:-900}"
# 打印带颜色的信息 # 打印带颜色的信息
info() { echo -e "${BLUE}[信息]${NC} $1"; } info() { echo -e "${BLUE}[信息]${NC} $1"; }
@@ -1127,6 +1129,16 @@ install_driver_manager() {
else else
sed -i "/DRIVER_MANAGER_PASSWORD=/a Environment=DRIVER_MANAGER_ALLOW_PRIVATE_ONLY=$DRIVER_MANAGER_PRIVATE_ONLY" /etc/systemd/system/cups-driver-manager.service sed -i "/DRIVER_MANAGER_PASSWORD=/a Environment=DRIVER_MANAGER_ALLOW_PRIVATE_ONLY=$DRIVER_MANAGER_PRIVATE_ONLY" /etc/systemd/system/cups-driver-manager.service
fi fi
if grep -q "DRIVER_MANAGER_MAX_UPLOAD_MB" /etc/systemd/system/cups-driver-manager.service; then
sed -i "s/DRIVER_MANAGER_MAX_UPLOAD_MB=.*/DRIVER_MANAGER_MAX_UPLOAD_MB=$DRIVER_MANAGER_MAX_UPLOAD_MB/" /etc/systemd/system/cups-driver-manager.service
else
sed -i "/DRIVER_MANAGER_ALLOW_PRIVATE_ONLY=/a Environment=DRIVER_MANAGER_MAX_UPLOAD_MB=$DRIVER_MANAGER_MAX_UPLOAD_MB" /etc/systemd/system/cups-driver-manager.service
fi
if grep -q "DRIVER_MANAGER_COMMAND_TIMEOUT" /etc/systemd/system/cups-driver-manager.service; then
sed -i "s/DRIVER_MANAGER_COMMAND_TIMEOUT=.*/DRIVER_MANAGER_COMMAND_TIMEOUT=$DRIVER_MANAGER_COMMAND_TIMEOUT/" /etc/systemd/system/cups-driver-manager.service
else
sed -i "/DRIVER_MANAGER_MAX_UPLOAD_MB=/a Environment=DRIVER_MANAGER_COMMAND_TIMEOUT=$DRIVER_MANAGER_COMMAND_TIMEOUT" /etc/systemd/system/cups-driver-manager.service
fi
else else
# 手动创建服务文件 # 手动创建服务文件
cat > /etc/systemd/system/cups-driver-manager.service << EOF cat > /etc/systemd/system/cups-driver-manager.service << EOF
@@ -1145,6 +1157,8 @@ RestartSec=5
Environment="DRIVER_MANAGER_USERNAME=$service_user" Environment="DRIVER_MANAGER_USERNAME=$service_user"
Environment="DRIVER_MANAGER_PASSWORD=$service_pass" Environment="DRIVER_MANAGER_PASSWORD=$service_pass"
Environment=DRIVER_MANAGER_ALLOW_PRIVATE_ONLY=$DRIVER_MANAGER_PRIVATE_ONLY Environment=DRIVER_MANAGER_ALLOW_PRIVATE_ONLY=$DRIVER_MANAGER_PRIVATE_ONLY
Environment=DRIVER_MANAGER_MAX_UPLOAD_MB=$DRIVER_MANAGER_MAX_UPLOAD_MB
Environment=DRIVER_MANAGER_COMMAND_TIMEOUT=$DRIVER_MANAGER_COMMAND_TIMEOUT
[Install] [Install]
WantedBy=multi-user.target WantedBy=multi-user.target