#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ CUPS 打印机驱动管理器 轻量级Web服务,用于上传和安装打印机驱动 """ import os import sys import subprocess import tempfile import shutil import tarfile import zipfile import gzip import ipaddress import uuid from pathlib import Path from functools import wraps from flask import Flask, request, render_template_string, redirect, url_for, flash, jsonify, Response from werkzeug.utils import secure_filename app = Flask(__name__) 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' 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'} # 管理员凭据(可通过环境变量设置) ADMIN_USERNAME = os.environ.get('DRIVER_MANAGER_USERNAME', 'admin') ADMIN_PASSWORD = os.environ.get('DRIVER_MANAGER_PASSWORD', 'admin') ALLOW_PRIVATE_ONLY = os.environ.get('DRIVER_MANAGER_ALLOW_PRIVATE_ONLY', '1').lower() not in ('0', 'false', 'no', 'off') app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER app.config['MAX_CONTENT_LENGTH'] = MAX_CONTENT_LENGTH # 确保上传目录存在 os.makedirs(UPLOAD_FOLDER, exist_ok=True) @app.before_request def limit_private_network(): """默认只允许本机、链路本地和内网地址访问。""" if not ALLOW_PRIVATE_ONLY: return None remote_addr = request.remote_addr or '' try: addr = ipaddress.ip_address(remote_addr) except ValueError: return Response('仅允许内网访问驱动管理器\n', 403) if addr.is_loopback or addr.is_private or addr.is_link_local: return None return Response('仅允许内网访问驱动管理器\n', 403) def _is_within_directory(base_dir, target_path): base = os.path.realpath(base_dir) target = os.path.realpath(target_path) return os.path.commonpath([base, target]) == base def safe_extract_tar(tar, path): """防止恶意 tar 包写出解压目录。""" for member in tar.getmembers(): target_path = os.path.join(path, member.name) if not _is_within_directory(path, target_path): raise ValueError(f'压缩包包含非法路径: {member.name}') if member.issym() or member.islnk(): raise ValueError(f'压缩包包含链接文件: {member.name}') tar.extractall(path) def safe_extract_zip(zip_ref, path): """防止恶意 zip 包写出解压目录。""" for member in zip_ref.infolist(): target_path = os.path.join(path, member.filename) if not _is_within_directory(path, target_path): raise ValueError(f'压缩包包含非法路径: {member.filename}') zip_ref.extractall(path) def check_auth(username, password): """验证用户名和密码""" return username == ADMIN_USERNAME and password == ADMIN_PASSWORD def authenticate(): """发送401响应""" return Response( '需要登录才能访问驱动管理器\n', 401, {'WWW-Authenticate': 'Basic realm="CUPS Driver Manager"'} ) def requires_auth(f): """认证装饰器""" @wraps(f) def decorated(*args, **kwargs): auth = request.authorization if not auth or not check_auth(auth.username, auth.password): return authenticate() return f(*args, **kwargs) return decorated def allowed_file(filename): """检查文件类型是否允许""" if '.' not in filename: return False file_type = get_file_type(filename) if file_type == 'unknown': return False ext = filename.rsplit('.', 1)[1].lower() return ext in ALLOWED_EXTENSIONS or file_type in ('tar.gz', 'ppd') def get_file_type(filename): """获取文件类型""" filename_lower = filename.lower() if filename_lower.endswith('.deb'): return 'deb' elif filename_lower.endswith('.ppd') or filename_lower.endswith('.ppd.gz'): return 'ppd' elif filename_lower.endswith('.tar.gz') or filename_lower.endswith('.tgz'): return 'tar.gz' elif filename_lower.endswith('.tar'): return 'tar' elif filename_lower.endswith('.zip'): return 'zip' elif filename_lower.endswith('.rpm'): return 'rpm' elif filename_lower.endswith('.sh') or filename_lower.endswith('.run'): return 'script' else: return 'unknown' 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: if shell: 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: result = subprocess.run( cmd, capture_output=True, text=True, timeout=COMMAND_TIMEOUT, cwd=cwd, input=input_text, encoding='utf-8', errors='replace' ) return { 'success': result.returncode == 0, 'stdout': result.stdout, 'stderr': result.stderr, 'returncode': result.returncode } except subprocess.TimeoutExpired: return {'success': False, 'stdout': '', 'stderr': f'命令执行超时(>{COMMAND_TIMEOUT}秒)', 'returncode': -1} except Exception as e: return {'success': False, 'stdout': '', 'stderr': str(e), 'returncode': -1} def install_deb(filepath): """安装 .deb 包""" results = [] result = run_command(['dpkg', '-i', filepath]) if result['success']: results.append(('安装 DEB 包', result)) return results # dpkg 因依赖缺失失败是常见情况,继续修复依赖后重试,不把初次失败计为最终失败。 results.append(('初次安装 DEB 包', { 'success': True, 'stdout': result['stdout'], 'stderr': '初次 dpkg 安装未完成,正在尝试自动修复依赖后重试。\n' + result['stderr'], 'returncode': 0 })) fix_result = run_command(['apt-get', 'install', '-f', '-y']) results.append(('修复 DEB 依赖', fix_result)) if fix_result['success']: retry_result = run_command(['dpkg', '-i', filepath]) results.append(('重新安装 DEB 包', retry_result)) return results def install_ppd(filepath): """安装 .ppd 文件""" results = [] # PPD文件目录 ppd_dirs = [ '/usr/share/ppd/custom', '/usr/share/cups/model' ] # 确保目录存在 for ppd_dir in ppd_dirs: os.makedirs(ppd_dir, exist_ok=True) filename = os.path.basename(filepath) 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: if is_gzipped: with gzip.open(filepath, 'rb') as src, open(dest, 'wb') as dst: shutil.copyfileobj(src, dst) else: shutil.copy2(filepath, dest) os.chmod(dest, 0o644) results.append((f'安装 PPD 到 {ppd_dir}', { 'success': True, 'stdout': f'已安装到 {dest}', 'stderr': '', 'returncode': 0 })) except Exception as e: results.append((f'安装 PPD 到 {ppd_dir}', { 'success': False, 'stdout': '', 'stderr': str(e), 'returncode': 1 })) return results def install_extracted_dir(extract_dir): """从已解压目录中自动寻找可安装的驱动文件。""" 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_') try: with tarfile.open(filepath, 'r:*') as tar: safe_extract_tar(tar, extract_dir) results.append(('解压文件', { 'success': True, 'stdout': f'已解压到 {extract_dir}', 'stderr': '', 'returncode': 0 })) results.extend(install_extracted_dir(extract_dir)) except Exception as e: results.append(('解压文件', { 'success': False, 'stdout': '', 'stderr': str(e), 'returncode': 1 })) finally: # 清理临时目录 shutil.rmtree(extract_dir, ignore_errors=True) return results def install_zip(filepath): """安装 .zip 包""" results = [] # 创建临时解压目录 extract_dir = tempfile.mkdtemp(prefix='driver_') try: # 解压 with zipfile.ZipFile(filepath, 'r') as zip_ref: safe_extract_zip(zip_ref, extract_dir) results.append(('解压文件', { 'success': True, 'stdout': f'已解压到 {extract_dir}', 'stderr': '', 'returncode': 0 })) results.extend(install_extracted_dir(extract_dir)) except Exception as e: results.append(('解压文件', { 'success': False, 'stdout': '', 'stderr': str(e), 'returncode': 1 })) finally: # 清理临时目录 shutil.rmtree(extract_dir, ignore_errors=True) return results def install_rpm(filepath): """安装 .rpm 包(转换为deb后安装)""" results = [] # 检查 alien 是否安装 alien_check = run_command(['which', 'alien']) if not alien_check['success']: # 安装 alien install_result = run_command(['apt-get', 'install', '-y', 'alien']) results.append(('安装alien工具', install_result)) if not install_result['success']: return results # 使用 alien 转换 work_dir = os.path.dirname(filepath) convert_result = run_command(['alien', '-d', filepath], cwd=work_dir) results.append(('转换RPM为DEB', convert_result)) if convert_result['success']: # 查找生成的 deb 文件 deb_files = list(Path(work_dir).glob('*.deb')) for deb_file in deb_files: deb_results = install_deb(str(deb_file)) results.extend(deb_results) os.remove(deb_file) # 清理临时deb文件 return results def install_script(filepath): """执行安装脚本""" results = [] filename = os.path.basename(filepath).lower() os.chmod(filepath, 0o755) # HP 插件需要特殊处理(非交互式安装) if 'hplip' in filename and 'plugin' in filename: # 直接执行 .run 文件,使用 yes 自动确认交互式提示 # 注意:hp-plugin 命令需要 .asc 签名文件,比较麻烦,所以直接执行 result = run_command(['/bin/sh', filepath], input_text='y\n' * 20) results.append(('执行 HP 插件安装脚本', result)) else: # 普通脚本直接执行,避免文件名包含空格时 shell 拼接失败。 if filename.endswith('.sh'): result = run_command(['/bin/sh', filepath]) else: result = run_command([filepath]) results.append(('执行安装脚本', result)) return results def install_driver(filepath, file_type): """根据文件类型安装驱动""" if file_type == 'deb': return install_deb(filepath) elif file_type == 'ppd': return install_ppd(filepath) elif file_type in ('tar.gz', 'tar', 'tgz'): return install_tar_gz(filepath) elif file_type == 'zip': return install_zip(filepath) elif file_type == 'rpm': return install_rpm(filepath) elif file_type == 'script': return install_script(filepath) else: return [('未知类型', { 'success': False, 'stdout': '', 'stderr': f'不支持的文件类型: {file_type}', 'returncode': 1 })] # HTML模板 HTML_TEMPLATE = '''
正在安装驱动,请稍候...
暂无自定义PPD文件
{% endif %}暂无自定义模型文件
{% endif %}未找到打印机相关软件包
{% endif %}