|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | +""" |
| 4 | +VulnShop 基础处理器 - 公共方法和工具函数 |
| 5 | +
|
| 6 | +提供所有处理器共享的响应发送、请求解析等公共功能 |
| 7 | +""" |
| 8 | + |
| 9 | +import json |
| 10 | +import os |
| 11 | +import time |
| 12 | +import xml.etree.ElementTree as ET |
| 13 | +from urllib.parse import unquote |
| 14 | +from datetime import datetime |
| 15 | + |
| 16 | + |
| 17 | +class BaseHandlerMixin: |
| 18 | + """基础处理器Mixin - 提供公共方法""" |
| 19 | + |
| 20 | + # 静态文件MIME类型 |
| 21 | + MIME_TYPES = { |
| 22 | + '.html': 'text/html; charset=utf-8', |
| 23 | + '.css': 'text/css; charset=utf-8', |
| 24 | + '.js': 'application/javascript; charset=utf-8', |
| 25 | + '.json': 'application/json; charset=utf-8', |
| 26 | + '.png': 'image/png', |
| 27 | + '.jpg': 'image/jpeg', |
| 28 | + '.jpeg': 'image/jpeg', |
| 29 | + '.gif': 'image/gif', |
| 30 | + '.ico': 'image/x-icon', |
| 31 | + '.svg': 'image/svg+xml', |
| 32 | + } |
| 33 | + |
| 34 | + def log_message(self, format, *args): |
| 35 | + """自定义日志格式""" |
| 36 | + from config import LOG_REQUESTS, LOG_FILE |
| 37 | + if LOG_REQUESTS: |
| 38 | + timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S') |
| 39 | + message = f"[{timestamp}] {self.address_string()} - {format % args}" |
| 40 | + print(message) |
| 41 | + try: |
| 42 | + os.makedirs(os.path.dirname(LOG_FILE), exist_ok=True) |
| 43 | + with open(LOG_FILE, 'a', encoding='utf-8') as f: |
| 44 | + f.write(message + '\n') |
| 45 | + except: |
| 46 | + pass |
| 47 | + |
| 48 | + def send_json_response(self, data, status=200): |
| 49 | + """发送JSON响应""" |
| 50 | + self.send_response(status) |
| 51 | + self.send_header('Content-Type', 'application/json; charset=utf-8') |
| 52 | + self.send_header('Access-Control-Allow-Origin', '*') |
| 53 | + self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') |
| 54 | + self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization') |
| 55 | + self.end_headers() |
| 56 | + response = json.dumps(data, ensure_ascii=False) |
| 57 | + self.wfile.write(response.encode('utf-8')) |
| 58 | + |
| 59 | + def send_error_response(self, message, status=400, sql_error=None): |
| 60 | + """发送错误响应(可能包含SQL错误信息用于演示)""" |
| 61 | + from config import DEBUG, DIFFICULTY |
| 62 | + data = { |
| 63 | + 'success': False, |
| 64 | + 'message': message, |
| 65 | + 'timestamp': int(time.time() * 1000) |
| 66 | + } |
| 67 | + if sql_error and DEBUG: |
| 68 | + data['debug'] = { |
| 69 | + 'sql_error': str(sql_error), |
| 70 | + 'difficulty': DIFFICULTY |
| 71 | + } |
| 72 | + self.send_json_response(data, status) |
| 73 | + |
| 74 | + def send_xml_response(self, data, status=200): |
| 75 | + """发送XML响应""" |
| 76 | + self.send_response(status) |
| 77 | + self.send_header('Content-Type', 'application/xml; charset=utf-8') |
| 78 | + self.send_header('Access-Control-Allow-Origin', '*') |
| 79 | + self.send_header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS') |
| 80 | + self.send_header('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Session-Id, X-Token') |
| 81 | + self.end_headers() |
| 82 | + |
| 83 | + # 将dict转换为XML |
| 84 | + def dict_to_xml(d, root_name='response'): |
| 85 | + root = ET.Element(root_name) |
| 86 | + for key, value in d.items(): |
| 87 | + child = ET.SubElement(root, key) |
| 88 | + if isinstance(value, dict): |
| 89 | + for k, v in value.items(): |
| 90 | + sub = ET.SubElement(child, k) |
| 91 | + sub.text = str(v) if v is not None else '' |
| 92 | + elif isinstance(value, list): |
| 93 | + for item in value: |
| 94 | + item_el = ET.SubElement(child, 'item') |
| 95 | + if isinstance(item, dict): |
| 96 | + for k, v in item.items(): |
| 97 | + sub = ET.SubElement(item_el, k) |
| 98 | + sub.text = str(v) if v is not None else '' |
| 99 | + else: |
| 100 | + item_el.text = str(item) |
| 101 | + else: |
| 102 | + child.text = str(value) if value is not None else '' |
| 103 | + return ET.tostring(root, encoding='unicode') |
| 104 | + |
| 105 | + xml_str = '<?xml version="1.0" encoding="UTF-8"?>\n' + dict_to_xml(data) |
| 106 | + self.wfile.write(xml_str.encode('utf-8')) |
| 107 | + |
| 108 | + def send_static_file(self, filepath): |
| 109 | + """发送静态文件""" |
| 110 | + static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'static') |
| 111 | + full_path = os.path.normpath(os.path.join(static_dir, filepath)) |
| 112 | + |
| 113 | + # 防止目录遍历 |
| 114 | + if not full_path.startswith(static_dir): |
| 115 | + self.send_error(403, 'Forbidden') |
| 116 | + return |
| 117 | + |
| 118 | + if not os.path.exists(full_path): |
| 119 | + self.send_error(404, 'File Not Found') |
| 120 | + return |
| 121 | + |
| 122 | + ext = os.path.splitext(filepath)[1].lower() |
| 123 | + mime_type = self.MIME_TYPES.get(ext, 'application/octet-stream') |
| 124 | + |
| 125 | + try: |
| 126 | + with open(full_path, 'rb') as f: |
| 127 | + content = f.read() |
| 128 | + self.send_response(200) |
| 129 | + self.send_header('Content-Type', mime_type) |
| 130 | + self.send_header('Content-Length', len(content)) |
| 131 | + self.end_headers() |
| 132 | + self.wfile.write(content) |
| 133 | + except Exception as e: |
| 134 | + self.send_error(500, str(e)) |
| 135 | + |
| 136 | + def get_post_data(self): |
| 137 | + """获取POST请求数据,支持JSON、URL-encoded和XML格式""" |
| 138 | + content_length = int(self.headers.get('Content-Length', 0)) |
| 139 | + if content_length == 0: |
| 140 | + return {} |
| 141 | + |
| 142 | + post_data = self.rfile.read(content_length).decode('utf-8') |
| 143 | + content_type = self.headers.get('Content-Type', '') |
| 144 | + |
| 145 | + if 'application/json' in content_type: |
| 146 | + try: |
| 147 | + return json.loads(post_data) |
| 148 | + except: |
| 149 | + return {} |
| 150 | + elif 'application/xml' in content_type or 'text/xml' in content_type: |
| 151 | + # XML解析 |
| 152 | + try: |
| 153 | + root = ET.fromstring(post_data) |
| 154 | + result = {} |
| 155 | + for child in root: |
| 156 | + result[child.tag] = child.text or '' |
| 157 | + return result |
| 158 | + except: |
| 159 | + return {} |
| 160 | + else: |
| 161 | + # application/x-www-form-urlencoded |
| 162 | + result = {} |
| 163 | + for pair in post_data.split('&'): |
| 164 | + if '=' in pair: |
| 165 | + key, value = pair.split('=', 1) |
| 166 | + result[unquote(key)] = unquote(value) |
| 167 | + return result |
| 168 | + |
| 169 | + def get_content_type(self): |
| 170 | + """获取请求的Content-Type类型""" |
| 171 | + content_type = self.headers.get('Content-Type', '') |
| 172 | + if 'application/json' in content_type: |
| 173 | + return 'json' |
| 174 | + elif 'application/xml' in content_type or 'text/xml' in content_type: |
| 175 | + return 'xml' |
| 176 | + else: |
| 177 | + return 'form' |
0 commit comments