简单的http抓包解包完整代码

之前用 FIFO 匹配(响应匹配第一个 pending 请求),但 TCP 分段导致响应包交错时,响应2可能先于响应1到达,FIFO 就会错配。

修复方案:ack 精确匹配

TCP 可靠传输保证: 响应的 ack = 请求的 seq + 请求 payload 长度 (ack 指向下一个期望接收的字节)。

有个小问题,作者只是解析的ipv4,没解析ipv6,如果你nginx设置的localhost然后localhost里边的hosts设置有ipv4和ipv6 2种,可能你访问的nginx的https会轮询匹配你的ipv4和v62种地址这样你会怀疑你抓包的2个接口只抓到1个,这样需要你改nginx的地址为localhost改为127.0.0.1,这样nginx只会代理到ipv4,然后解析就正常了,或者尝试改我给你的解析代码支持ipv6,因为作者只是测试,就不改了。

如果你嫌debug打印的多,可以把代码种的debug去掉,作者是为了更直观的看输出,解决问题打印的。

最终代码:

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import psutil
import time
import socket
import struct
import json
import gzip
import io
from datetime import datetime
from collections import defaultdict, deque
import re

class FullAPITrafficAnalyzer:
    """修复版 - 使用TCP序列号匹配同一连接的多个请求"""
    
    def __init__(self, target_interface=None, monitor_ports=None, target_api_paths=None):
        self.target_interface = target_interface
        # 监控端口列表,默认常见HTTP端口,可通过参数传入
        self.monitor_ports = monitor_ports if monitor_ports else [80, 443, 8080, 8000, 8111, 8443, 81]
        # 目标API路径前缀列表,支持多个,命中任一即捕获
        self.target_api_paths = target_api_paths if target_api_paths else ['/yjz_api_audit', '/webserver/api']
        
        # 存储未匹配的请求 - 使用 (src_ip, src_port, dest_ip, dest_port, seq) 作为key
        self.pending_requests = {}  # key: (src_ip, src_port, dest_ip, dest_port, seq)
        
        # 存储每个连接的最后seq,用于匹配响应
        self.connection_seq = defaultdict(dict)  # (src_ip, src_port, dest_ip, dest_port) -> {req_seq: request_data}
        
        self.http_methods = {
            'GET', 'POST', 'PUT', 'DELETE', 'PATCH', 
            'HEAD', 'OPTIONS', 'CONNECT', 'TRACE'
        }
        
        self.running = True
        self.request_counter = 0
        
        # 统计
        self.stats = {
            'total_requests': 0,
            'api_requests': 0,
            'by_method': defaultdict(int),
            'by_status': defaultdict(int),
            'by_endpoint': defaultdict(int),
            'request_response_pairs': deque(maxlen=100),
            'start_time': datetime.now()
        }
        
    def _decode_chunked(self, body_bytes):
        """解码chunked传输编码,拼接所有chunk的数据部分(字节层面操作)"""
        try:
            result = b''
            remaining = body_bytes
            while remaining:
                # 找到chunk长度行(字节层面查找\r\n)
                line_end = remaining.find(b'\r\n')
                if line_end == -1:
                    break

                # 解析chunk长度(十六进制)
                size_str = remaining[:line_end].strip()
                # 忽略chunk扩展(分号后的内容)
                sep = size_str.find(b';')
                if sep != -1:
                    size_str = size_str[:sep].strip()

                try:
                    chunk_size = int(size_str, 16)
                except:
                    # 不是合法的chunk长度行,当作普通body返回
                    return body_bytes

                if chunk_size == 0:
                    break  # 结束chunk

                # 跳过长度行,取chunk数据
                remaining = remaining[line_end + 2:]
                chunk_data = remaining[:chunk_size]
                result += chunk_data
                # 跳过chunk数据 + \r\n
                remaining = remaining[chunk_size + 2:]

            return result
        except:
            return body_bytes

    def _decode_gzip(self, body_bytes):
        """解压gzip压缩的响应体,还原明文(字节层面操作)"""
        try:
            decompressed = gzip.decompress(body_bytes)
            return decompressed
        except:
            # 解压失败(可能数据不完整或被TCP分段),返回原始bytes
            return body_bytes

    def parse_http_messages(self, data):
        """解析payload中可能包含的多个HTTP消息(同一TCP包里可能有多个响应)
        在字节层面扫描,只检查行首是否为HTTP方法或HTTP/版本,避免二进制body内容误匹配"""
        if not data:
            return []

        # 在字节层面按\r\n分割,但保留原始字节引用
        # 找到所有HTTP消息起始位置(字节偏移)
        positions = []
        for method in self.http_methods:
            method_bytes = method.encode('ascii') + b' '
            start = 0
            while True:
                pos = data.find(method_bytes, start)
                if pos == -1:
                    break
                # 确保是行首(开头或前面是\r\n)
                if pos == 0 or data[pos-2:pos] == b'\r\n':
                    positions.append(pos)
                start = pos + 1

        # 查找响应起始位置 HTTP/x.x
        start = 0
        while True:
            pos = data.find(b'HTTP/', start)
            if pos == -1:
                break
            if pos == 0 or data[pos-2:pos] == b'\r\n':
                positions.append(pos)
            start = pos + 1

        positions = sorted(set(positions))
        if not positions:
            return []

        messages = []
        for i, start_pos in enumerate(positions):
            end_pos = positions[i + 1] if i + 1 < len(positions) else len(data)
            segment = data[start_pos:end_pos]
            msg = self._parse_single_http_message(segment)
            if msg:
                messages.append(msg)

        # 调试: 打印解析结果
        if messages:
            print(f"\n[DEBUG parse_http_messages] 找到 {len(messages)} 个HTTP消息:")
            for i, m in enumerate(messages):
                if m['type'] == 'request':
                    print(f"  [{i}] 请求: {m['method']} {m.get('path', '?')}")
                else:
                    print(f"  [{i}] 响应: {m.get('status_code', '?')}")

        return messages

    def _parse_single_http_message(self, data_bytes):
        """解析单个HTTP消息(请求或响应),在字节层面操作以正确处理二进制body"""
        try:
            # 在字节层面找header和body的分界 \r\n\r\n
            header_end = data_bytes.find(b'\r\n\r\n')
            if header_end == -1:
                # 没有完整的header结束标记,尝试整个当作header
                header_part = data_bytes
                body_bytes = b''
            else:
                header_part = data_bytes[:header_end]
                body_bytes = data_bytes[header_end + 4:]

            # header部分是ASCII,可以安全decode
            try:
                header_text = header_part.decode('ascii', errors='ignore')
            except:
                header_text = header_part.decode('utf-8', errors='ignore')

            lines = header_text.split('\r\n')
            if not lines:
                return None

            first_line = lines[0]
            parts = first_line.split(' ')

            if len(parts) < 3:
                return None

            # 判断是请求还是响应
            if parts[0] in self.http_methods:
                method, path, version = parts[0], parts[1], parts[2]

                path_part = path
                query_string = ''
                if '?' in path:
                    path_part, query_string = path.split('?', 1)

                result = {
                    'type': 'request',
                    'method': method,
                    'path': path_part,
                    'full_path': path,
                    'query_string': query_string,
                    'version': version
                }
            elif parts[0].startswith('HTTP/'):
                version, status_code = parts[0], parts[1]
                status_text = ' '.join(parts[2:]) if len(parts) > 2 else ''

                try:
                    status_code_int = int(status_code)
                except:
                    return None

                result = {
                    'type': 'response',
                    'version': version,
                    'status_code': status_code_int,
                    'status_text': status_text
                }
            else:
                return None

            # 解析headers
            headers = {}
            for line in lines[1:]:
                if ': ' in line:
                    key, value = line.split(': ', 1)
                    headers[key.lower()] = value

            result['headers'] = headers

            # 判断响应body是否完整(TCP分段可能导致body不完整)
            is_body_complete = True
            if result['type'] == 'response':
                if headers.get('transfer-encoding', '').lower() == 'chunked':
                    # chunked: 检查结束标记 0\r\n\r\n
                    if body_bytes.find(b'\r\n0\r\n\r\n') == -1 and body_bytes.find(b'\r\n0\r\n') == -1:
                        is_body_complete = False
                elif 'content-length' in headers:
                    # 有content-length: 检查body长度
                    try:
                        expected_len = int(headers['content-length'])
                        if len(body_bytes) < expected_len:
                            is_body_complete = False
                    except:
                        pass
                elif headers.get('connection', '').lower() == 'close':
                    # 连接关闭模式,body到连接结束,单包无法判断
                    pass
            result['is_body_complete'] = is_body_complete

            # 处理chunked编码 - 在字节层面解码
            if headers.get('transfer-encoding', '').lower() == 'chunked':
                body_bytes = self._decode_chunked(body_bytes)

            # 处理gzip压缩 - 在字节层面解压
            if headers.get('content-encoding', '').lower() == 'gzip' and body_bytes:
                body_bytes = self._decode_gzip(body_bytes)

            # body转成字符串显示
            try:
                body = body_bytes.decode('utf-8', errors='ignore')
            except:
                body = body_bytes.decode('latin-1', errors='ignore')

            result['body'] = body

            # 尝试解析JSON
            if body:
                try:
                    json_body = json.loads(body)
                    result['json_body'] = json_body
                except:
                    pass

            # 检查是否包含目标API路径(匹配任一前缀)
            is_target_api = False
            if result['type'] == 'request':
                for api_path in self.target_api_paths:
                    if api_path in path:
                        is_target_api = True
                        break
            result['is_target_api'] = is_target_api

            return result

        except Exception as e:
            return None
    
    def parse_ip_packet(self, data):
        """解析IP包 - 自动识别链路层类型(Ethernet 14字节 / LINUX_SLL 16字节)"""
        try:
            if len(data) < 34:
                return None

            # 自动识别链路层类型
            # Ethernet: data[12:14] == 0x0800 (IPv4), IP头从14开始
            # LINUX_SLL: data[14:16] == 0x0800 (IPv4), IP头从16开始
            link_offset = None
            if len(data) >= 16:
                eth_type = struct.unpack('!H', data[12:14])[0]
                if eth_type == 0x0800:
                    link_offset = 14  # Ethernet
                else:
                    sll_type = struct.unpack('!H', data[14:16])[0]
                    if sll_type == 0x0800:
                        link_offset = 16  # LINUX_SLL

            if link_offset is None:
                return None

            ip_header = data[link_offset:link_offset + 20]
            if len(ip_header) < 20:
                return None
            iph = struct.unpack('!BBHHHBBH4s4s', ip_header)

            version_ihl = iph[0]
            ihl = version_ihl & 0xF

            return {
                'src_ip': socket.inet_ntoa(iph[8]),
                'dest_ip': socket.inet_ntoa(iph[9]),
                'protocol': iph[6],
                'header_len': ihl * 4,
                'total_len': iph[2],
                'link_offset': link_offset  # 传给传输层解析用
            }
        except:
            return None

    def parse_transport(self, data, ip_info):
        """解析传输层 - 特别注意获取seq"""
        try:
            # 使用链路层偏移 + IP头长度,而非硬编码14
            offset = ip_info['link_offset'] + ip_info['header_len']
            
            if ip_info['protocol'] == 6:  # TCP
                tcp_header = data[offset:offset+20]
                tcp = struct.unpack('!HHLLBBHHH', tcp_header)
                return {
                    'src_port': tcp[0],
                    'dest_port': tcp[1],
                    'seq': tcp[2],      # 序列号 - 用于匹配
                    'ack': tcp[3],      # 确认号
                    'header_len': (tcp[4] >> 4) * 4,
                    'flags': tcp[5] & 0x3F
                }
            return None
        except:
            return None
    
    def extract_payload(self, data, ip_info, transport_info):
        """提取payload数据"""
        try:
            # 使用链路层偏移 + IP头长度 + TCP头长度
            offset = ip_info['link_offset'] + ip_info['header_len'] + transport_info['header_len']
            return data[offset:]
        except:
            return None
    
    def print_api_request(self, http_info, ip_info, transport_info, request_id):
        """打印API请求"""
        endpoint = http_info['path'].split('/')[-1] if http_info['path'] else 'unknown'
        
        print(f"\n{'='*120}")
        print(f"📤 请求 #{request_id} [{datetime.now().strftime('%H:%M:%S.%f')[:-3]}]")
        print(f"{'='*120}")
        print(f"  方法: {http_info['method']}")
        print(f"  端点: {endpoint}")
        print(f"  路径: {http_info['full_path']}")
        print(f"  来源: {ip_info['src_ip']}:{transport_info['src_port']}")
        print(f"  目标: {ip_info['dest_ip']}:{transport_info['dest_port']}")
        print(f"  TCP Seq: {transport_info['seq']}")  # 显示seq用于调试
        
        # 显示Headers
        important_headers = ['host', 'user-agent', 'content-type', 'cookie']
        for key in important_headers:
            if key in http_info.get('headers', {}):
                value = http_info['headers'][key]
                if key == 'cookie':
                    value = value[:30] + '...' if len(value) > 30 else value
                print(f"  {key}: {value}")
        
        # 显示JSON Body
        if http_info.get('json_body'):
            print(f"\n  📦 请求JSON Body:")
            print(f"    {json.dumps(http_info['json_body'], ensure_ascii=False, indent=4)}")
        
        print(f"{'='*120}")
    
    def print_api_response(self, http_info, ip_info, transport_info, request_info=None):
        """打印API响应"""
        endpoint = request_info.get('request', {}).get('path', '').split('/')[-1] if request_info else 'unknown'
        
        print(f"\n{'='*120}")
        print(f"📥 响应 [{datetime.now().strftime('%H:%M:%S.%f')[:-3]}]")
        print(f"{'='*120}")
        print(f"  状态码: {http_info['status_code']} {http_info.get('status_text', '')}")
        print(f"  端点: {endpoint}")
        print(f"  来源: {ip_info['src_ip']}:{transport_info['src_port']}")
        print(f"  目标: {ip_info['dest_ip']}:{transport_info['dest_port']}")
        
        # 显示响应Headers
        if 'content-type' in http_info.get('headers', {}):
            print(f"  content-type: {http_info['headers']['content-type']}")
        
        # 显示响应JSON
        if http_info.get('json_body'):
            print(f"\n  📦 响应JSON Body (完整):")
            print(f"    {json.dumps(http_info['json_body'], ensure_ascii=False, indent=4)}")
        elif http_info.get('body'):
            clean_body = http_info['body'].strip()
            try:
                if clean_body and (clean_body.startswith('{') or clean_body.startswith('[')):
                    json_data = json.loads(clean_body)
                    print(f"\n  📦 响应JSON Body (完整):")
                    print(f"    {json.dumps(json_data, ensure_ascii=False, indent=4)}")
                else:
                    print(f"\n  📄 响应Body:")
                    print(f"    {clean_body[:500]}")
            except:
                print(f"\n  📄 响应Body:")
                print(f"    {clean_body[:500]}")
        
        # 显示响应时间
        if request_info:
            req_time = request_info.get('timestamp')
            if req_time:
                elapsed = (datetime.now() - req_time).total_seconds() * 1000
                print(f"\n  ⏱️  响应时间: {elapsed:.2f}ms")
            req = request_info.get('request', {})
            print(f"  🔗 对应请求: #{request_info.get('id')} {req.get('method')} {endpoint}")
        
        print(f"{'='*120}\n")
    
    def capture_packets(self, count=0):
        """捕获数据包"""
        try:
            sock = socket.socket(socket.AF_PACKET, socket.SOCK_RAW, socket.ntohs(3))
            
            if self.target_interface:
                sock.bind((self.target_interface, 0))
            
            print(f"\n{'='*120}")
            print(f"  🎯 API流量监控 (使用TCP Seq匹配)")
            print(f"  监控网卡: {self.target_interface or '所有网卡'}")
            print(f"  目标路径: {', '.join(self.target_api_paths)}")
            print(f"  监控端口: 80, 443, 8080, 8000, 8111, 8443")
            print(f"{'='*120}\n")
            
            captured = 0
            
            while self.running and (count == 0 or captured < count):
                try:
                    data, addr = sock.recvfrom(65535)
                    
                    ip_info = self.parse_ip_packet(data)
                    if not ip_info:
                        continue
                    
                    if ip_info['protocol'] == 6:
                        transport_info = self.parse_transport(data, ip_info)
                        if transport_info:
                            if (transport_info['src_port'] in self.monitor_ports or 
                                transport_info['dest_port'] in self.monitor_ports):
                                
                                payload = self.extract_payload(data, ip_info, transport_info)
                                if payload and len(payload) > 10:
                                    # 调试: 打印每个通过过滤的payload开头
                                    preview = payload[:120].decode('utf-8', errors='ignore').replace('\r', '\\r').replace('\n', '\\n')
                                    print(f"[DEBUG payload] {ip_info['src_ip']}:{transport_info['src_port']} -> {ip_info['dest_ip']}:{transport_info['dest_port']} seq={transport_info['seq']} len={len(payload)}")
                                    print(f"  开头: {preview}")

                                    payload_len = len(payload)
                                    http_messages = self.parse_http_messages(payload)
                                    for msg_idx, http_info in enumerate(http_messages):
                                        self.process_message(http_info, ip_info, transport_info, msg_idx, payload_len)
                                        
                                        if http_info['type'] == 'request' and http_info.get('is_target_api'):
                                            captured += 1
                                            if count > 0 and captured >= count:
                                                print(f"\n✅ 已捕获 {captured} 个API请求")
                                                break
                                    
                except KeyboardInterrupt:
                    print("\n\n⏹️  捕获已停止")
                    break
                except:
                    pass
            
            sock.close()
            self.print_summary()
            
        except PermissionError:
            print("❌ 错误: 需要root权限! 请使用: sudo python3 api_analyzer.py")
        except Exception as e:
            print(f"❌ 错误: {e}")
    
    def process_message(self, http_info, ip_info, transport_info, msg_idx=0, payload_len=0):
        """处理HTTP消息 - 使用seq匹配"""
        if http_info['type'] == 'request':
            # 只处理目标API请求(匹配任一前缀)
            if http_info.get('is_target_api'):
                # 使用TCP序列号+msg_idx作为唯一标识
                # msg_idx区分同一TCP包里的多个HTTP消息
                conn_key = (
                    ip_info['src_ip'],
                    transport_info['src_port'],
                    ip_info['dest_ip'],
                    transport_info['dest_port'],
                    transport_info['seq'],  # 使用seq区分同一连接的不同请求
                    msg_idx  # 区分同一TCP包里的多个HTTP消息
                )
                
                # 请求级别去重 - loopback上同一个包会被抓到两次(EGRESS+INGRESS)
                # 如果conn_key已存在,说明是重复抓包,跳过
                if conn_key in self.pending_requests:
                    print(f"[DEBUG dedup] 跳过重复请求: {http_info['method']} {http_info.get('path', '?')} seq={transport_info['seq']} msg_idx={msg_idx}")
                    return
                
                self.stats['api_requests'] += 1
                self.request_counter += 1
                request_id = self.request_counter
                
                endpoint = http_info['path'].split('/')[-1]
                self.stats['by_endpoint'][endpoint] += 1
                self.stats['by_method'][http_info['method']] += 1
                
                # 存储请求 - 记录seq和payload_len,用于响应的ack精确匹配
                # 期望的响应ack = 请求seq + payload_len(TCP可靠传输保证)
                self.pending_requests[conn_key] = {
                    'id': request_id,
                    'request': http_info,
                    'ip_info': ip_info,
                    'transport_info': transport_info,
                    'timestamp': datetime.now(),
                    'req_seq': transport_info['seq'],
                    'payload_len': payload_len
                }
                
                # 打印请求
                self.print_api_request(http_info, ip_info, transport_info, request_id)
                
                # 清理过期请求
                current_time = datetime.now()
                for key in list(self.pending_requests.keys()):
                    if (current_time - self.pending_requests[key]['timestamp']).total_seconds() > 30:
                        del self.pending_requests[key]
        
        elif http_info['type'] == 'response':
            # 匹配请求 - 用TCP ack精确匹配请求seq + payload_len
            matched_request = None
            matched_key = None

            # 响应的来源 = 请求的目标, 响应的目标 = 请求的来源
            # 响应的ack = 请求的seq + 请求payload长度(TCP可靠传输保证)
            response_ack = transport_info.get('ack', 0)

            for stored_key, request_data in list(self.pending_requests.items()):
                if len(stored_key) >= 6:
                    s_ip, s_port, d_ip, d_port, seq, m_idx = stored_key

                    # 检查IP和端口是否匹配(反向)
                    if (s_ip == ip_info['dest_ip'] and 
                        s_port == transport_info['dest_port'] and
                        d_ip == ip_info['src_ip'] and 
                        d_port == transport_info['src_port']):

                        # 用ack精确匹配: 响应的ack应该等于请求seq + payload_len
                        req_seq = request_data.get('req_seq', 0)
                        payload_len = request_data.get('payload_len', 0)
                        expected_ack = req_seq + payload_len

                        if response_ack == expected_ack:
                            # 精确匹配
                            matched_request = request_data
                            matched_key = stored_key
                            break

            # 如果精确匹配失败,回退到FIFO(同连接同端口最早存入的)
            if not matched_request:
                for stored_key, request_data in list(self.pending_requests.items()):
                    if len(stored_key) >= 6:
                        s_ip, s_port, d_ip, d_port, seq, m_idx = stored_key
                        if (s_ip == ip_info['dest_ip'] and 
                            s_port == transport_info['dest_port'] and
                            d_ip == ip_info['src_ip'] and 
                            d_port == transport_info['src_port']):
                            matched_request = request_data
                            matched_key = stored_key
                            break
            
            # 如果匹配到请求
            if matched_request:
                req_info = matched_request['request']
                
                if req_info.get('is_target_api'):
                    self.stats['by_status'][http_info['status_code']] += 1
                    
                    # 检查响应body是否完整
                    if http_info.get('is_body_complete', True):
                        # body完整,正常打印响应
                        self.print_api_response(http_info, ip_info, transport_info, matched_request)
                    else:
                        # body不完整(TCP分段截断),标记截断但仍消费请求保证后续配对正确
                        endpoint = req_info.get('path', '').split('/')[-1]
                        print(f"\n{'='*120}")
                        print(f"📥 响应 [截断] [{datetime.now().strftime('%H:%M:%S.%f')[:-3]}]")
                        print(f"{'='*120}")
                        print(f"  状态码: {http_info['status_code']} {http_info.get('status_text', '')}")
                        print(f"  端点: {endpoint}")
                        print(f"  ⚠️  响应body不完整(TCP分段截断),跳过body显示")
                        print(f"  🔗 对应请求: #{matched_request['id']} {req_info.get('method')} {endpoint}")
                        print(f"{'='*120}\n")
                    
                    # 保存配对
                    self.stats['request_response_pairs'].append({
                        'request_id': matched_request['id'],
                        'request': req_info,
                        'response': http_info,
                        'status_code': http_info['status_code'],
                        'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
                    })
                
                # 删除已匹配的请求
                if matched_key in self.pending_requests:
                    del self.pending_requests[matched_key]
    
    def print_summary(self):
        """打印统计摘要"""
        runtime = datetime.now() - self.stats['start_time']
        
        print(f"\n\n{'='*120}")
        print(f"  📊 API监控统计报告")
        print(f"{'='*120}")
        print(f"  运行时间: {runtime}")
        print(f"  API请求数: {self.stats['api_requests']}")
        
        if self.stats['by_endpoint']:
            print(f"\n  📌 API端点统计:")
            for endpoint, count in sorted(self.stats['by_endpoint'].items(), key=lambda x: x[1], reverse=True):
                print(f"    {endpoint}: {count} 次")
        
        if self.stats['by_status']:
            print(f"\n  📌 响应状态码分布:")
            for status, count in sorted(self.stats['by_status'].items(), key=lambda x: x[1], reverse=True):
                print(f"    {status}: {count} 次")
        
        if self.stats['request_response_pairs']:
            print(f"\n  🔄 请求-响应配对:")
            for pair in list(self.stats['request_response_pairs']):
                req = pair['request']
                endpoint = req.get('path', '').split('/')[-1]
                print(f"    #{pair['request_id']} {req['method']} {endpoint} -> {pair['status_code']}")
        
        print(f"{'='*120}\n")

if __name__ == "__main__":
    print("=" * 120)
    print("  🎯 API流量监控 (使用TCP Seq匹配同一连接的多个请求)")
    print("=" * 120)
    
    # 显示可用网卡
    print("\n📡 可用网卡:")
    net_io = psutil.net_io_counters(pernic=True)
    for iface in net_io.keys():
        counters = net_io[iface]
        def format_size(b):
            for unit in ['B', 'KB', 'MB', 'GB']:
                if b < 1024.0:
                    return f"{b:.2f} {unit}"
                b /= 1024.0
            return f"{b:.2f} TB"
        print(f"  • {iface}: 发送 {format_size(counters.bytes_sent)}, 接收 {format_size(counters.bytes_recv)}")
    
    print("\n💡 注意: 如果API在本地(127.0.0.1),选择 lo 网卡")
    print("💡 如果是远程API,选择 ens224 或其他物理网卡")
    
    interface = input("\n输入网卡名称: ").strip() or None
    ports_input = input("监控端口 (逗号分隔,留空用默认 80,443,81,8080,8000,8111,8443): ").strip()
    if ports_input:
        monitor_ports = [int(p.strip()) for p in ports_input.split(',') if p.strip()]
    else:
        monitor_ports = None
    count = input("捕获数量 (留空持续捕获): ").strip()
    count = int(count) if count else 0
    
    analyzer = FullAPITrafficAnalyzer(interface, monitor_ports)
    analyzer.capture_packets(count)

以上是完整的代码,前置条件

安装python3

安装psutil

如果安装psutil出问题,可以试试

sudo yum groupinstall -y "Development Tools"

sudo yum install -y python3-devel openssl-devel bzip2-devel libffi-devel zlib-devel

pip3 install --upgrade pip setuptools wheel

pip3 install psutil

pip install psutil

使用演示

  1. 启动分析程序

python api_analyzer.py

输入你的监控网卡,不输入监控所有,这个时候会有点多,有点乱

输入你的端口,不输入就是我默认给的端口

然后继续回车开始监控

  1. 开始打包

执行你tcpdump抓到的流量包

tcpreplay -i ens192 -t /tmp/api_capture.pcap

这里的ens192是你刚启动监听的网卡,这个pcap包是你抓的某个机器的http流量的包。

  1. 查看分析程序
python 复制代码
[DEBUG payload] 192.168.200.34:56702 -> 192.168.200.227:81 seq=3222663193 len=696
  开头: GET / HTTP/1.1\r\nHost: 192.168.200.227:81\r\nConnection: keep-alive\r\nCache-Control: max-age=0\r\nUpgrade-Insecure-Requests: 1

[DEBUG parse_http_messages] 找到 1 个HTTP消息:
  [0] 请求: GET /
[DEBUG payload] 192.168.200.227:81 -> 192.168.200.34:56702 seq=2708098702 len=173
  开头: HTTP/1.1 304 Not Modified\r\nServer: nginx\r\nDate: Thu, 20 Aug 2026 01:52:48 GMT\r\nLast-Modified: Mon, 03 Aug 2026 07:02:36 

[DEBUG parse_http_messages] 找到 1 个HTTP消息:
  [0] 响应: 304
[DEBUG payload] 192.168.200.34:56702 -> 192.168.200.227:81 seq=3222663889 len=643
  开头: POST /webserver/api/v3/nodeConfig/config/isPlatform HTTP/1.1\r\nHost: 192.168.200.227:81\r\nConnection: keep-alive\r\nContent-

[DEBUG parse_http_messages] 找到 1 个HTTP消息:
  [0] 请求: POST /webserver/api/v3/nodeConfig/config/isPlatform

========================================================================================================================
📤 请求 #1 [11:05:29.725]
========================================================================================================================
  方法: POST
  端点: isPlatform
  路径: /webserver/api/v3/nodeConfig/config/isPlatform
  来源: 192.168.200.34:56702
  目标: 192.168.200.227:81
  TCP Seq: 3222663889
  host: 192.168.200.227:81
  user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36
  cookie: dbAudit.sid=b6ee336c-59fb-4635...
========================================================================================================================
[DEBUG payload] 192.168.200.34:62980 -> 192.168.200.227:81 seq=2089186009 len=520
  开头: GET /oemAssets/logo.png?t=1787191664928 HTTP/1.1\r\nHost: 192.168.200.227:81\r\nConnection: keep-alive\r\nUser-Agent: Mozilla/

[DEBUG parse_http_messages] 找到 1 个HTTP消息:
  [0] 请求: GET /oemAssets/logo.png
[DEBUG payload] 192.168.200.227:81 -> 192.168.200.34:56702 seq=2708098875 len=751
  开头: HTTP/1.1 200 \r\nServer: nginx\r\nDate: Thu, 20 Aug 2026 01:52:48 GMT\r\nContent-Type: application/json;charset=UTF-8\r\nTransfe

[DEBUG parse_http_messages] 找到 1 个HTTP消息:
  [0] 响应: 200

========================================================================================================================
📥 响应 [11:05:29.725]
========================================================================================================================
  状态码: 200 
  端点: isPlatform
  来源: 192.168.200.227:81
  目标: 192.168.200.34:56702
  content-type: application/json;charset=UTF-8

  📦 响应JSON Body (完整):
    {
    "code": 200,
    "tips": "成功",
    "attribute": {},
    "data": false,
    "timestamp": 1787190768296,
    "success": true
}

  ⏱️  响应时间: 0.78ms
  🔗 对应请求: #1 POST isPlatform
========================================================================================================================

[DEBUG payload] 192.168.200.227:81 -> 192.168.200.34:62980 seq=2024175248 len=1260
  开头: 9~|#-YO'+ۗN˼j>+;]29<rLM__NkKyݶw9IvEYV.yE4-+ZlE&˹#>լF avŰiJ
[DEBUG payload] 192.168.200.34:62980 -> 192.168.200.227:81 seq=2089186529 len=708
  开头: POST /webserver/api/v3/my/info HTTP/1.1\r\nHost: 192.168.200.227:81\r\nConnection: keep-alive\r\nContent-Length: 36\r\nUser-Agen

[DEBUG parse_http_messages] 找到 1 个HTTP消息:
  [0] 请求: POST /webserver/api/v3/my/info

========================================================================================================================
📤 请求 #2 [11:05:29.726]
========================================================================================================================
  方法: POST
  端点: info
  路径: /webserver/api/v3/my/info
  来源: 192.168.200.34:62980
  目标: 192.168.200.227:81
  TCP Seq: 2089186529
  host: 192.168.200.227:81
  user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36
  content-type: application/x-www-form-urlencoded
  cookie: dbAudit.sid=b6ee336c-59fb-4635...
========================================================================================================================
[DEBUG payload] 192.168.200.227:81 -> 192.168.200.34:62980 seq=2024179428 len=1048
  开头: X       Y}KZnF\r=(>[[bDO"\ro:Eɗ<zxWrDfC
                                               6fEҝ݈wpM>xo
                                                         ׁ]

[DEBUG payload] 192.168.200.34:62980 -> 192.168.200.227:81 seq=2089187237 len=573
  开头: GET /webserver/api/v3/global/config HTTP/1.1\r\nHost: 192.168.200.227:81\r\nConnection: keep-alive\r\nUser-Agent: Mozilla/5.0 

[DEBUG parse_http_messages] 找到 1 个HTTP消息:
  [0] 请求: GET /webserver/api/v3/global/config

========================================================================================================================
📤 请求 #3 [11:05:29.726]
========================================================================================================================
  方法: GET
  端点: config
  路径: /webserver/api/v3/global/config
  来源: 192.168.200.34:62980
  目标: 192.168.200.227:81
  TCP Seq: 2089187237
  host: 192.168.200.227:81
  user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36
  cookie: dbAudit.sid=b6ee336c-59fb-4635...
========================================================================================================================
[DEBUG payload] 192.168.200.227:81 -> 192.168.200.34:62980 seq=2024180476 len=667
  开头: HTTP/1.1 200 \r\nServer: nginx\r\nDate: Thu, 20 Aug 2026 01:52:48 GMT\r\nContent-Type: application/json;charset=UTF-8\r\nTransfe

[DEBUG parse_http_messages] 找到 1 个HTTP消息:
  [0] 响应: 200

========================================================================================================================
📥 响应 [11:05:29.727]
========================================================================================================================
  状态码: 200 
  端点: config
  来源: 192.168.200.227:81
  目标: 192.168.200.34:62980
  content-type: application/json;charset=UTF-8

  📦 响应JSON Body (完整):
    {
    "code": 200,
    "tips": "成功",
    "attribute": {},
    "data": {
        "minPasswordLength": 8,
        "maxPasswordLength": 32,
        "pageSessionTimeInMinute": 15,
        "passwordComplexity": 3
    },
    "timestamp": 1787190768540,
    "success": true
}

  ⏱️  响应时间: 0.64ms
  🔗 对应请求: #3 GET config
========================================================================================================================

[DEBUG payload] 192.168.200.34:54105 -> 192.168.200.227:81 seq=1039551939 len=647
  开头: GET /webserver/websocket/v3/installCount HTTP/1.1\r\nHost: 192.168.200.227:81\r\nConnection: Upgrade\r\nPragma: no-cache\r\nCach

[DEBUG parse_http_messages] 找到 1 个HTTP消息:
  [0] 请求: GET /webserver/websocket/v3/installCount
[DEBUG payload] 192.168.200.227:81 -> 192.168.200.34:54105 seq=262463226 len=502
  开头: HTTP/1.1 101 \r\nServer: nginx\r\nDate: Thu, 20 Aug 2026 01:52:48 GMT\r\nConnection: upgrade\r\nAccess-control-Allow-Origin: *\r\n

[DEBUG parse_http_messages] 找到 1 个HTTP消息:
  [0] 响应: 101
[DEBUG payload] 192.168.200.34:62980 -> 192.168.200.227:81 seq=2089187810 len=592
  开头: GET /webserver/api/v3/database/setting/serverGroupList HTTP/1.1\r\nHost: 192.168.200.227:81\r\nConnection: keep-alive\r\nUser-

[DEBUG parse_http_messages] 找到 1 个HTTP消息:
  [0] 请求: GET /webserver/api/v3/database/setting/serverGroupList

========================================================================================================================
📤 请求 #4 [11:05:29.728]
========================================================================================================================
  方法: GET
  端点: serverGroupList
  路径: /webserver/api/v3/database/setting/serverGroupList
  来源: 192.168.200.34:62980
  目标: 192.168.200.227:81
  TCP Seq: 2089187810
  host: 192.168.200.227:81
  user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36
  cookie: dbAudit.sid=b6ee336c-59fb-4635...
========================================================================================================================
[DEBUG payload] 192.168.200.34:55569 -> 192.168.200.227:81 seq=2884300098 len=639
  开头: GET /webserver/websocket/v3/date HTTP/1.1\r\nHost: 192.168.200.227:81\r\nConnection: Upgrade\r\nPragma: no-cache\r\nCache-Contro

[DEBUG parse_http_messages] 找到 1 个HTTP消息:
  [0] 请求: GET /webserver/websocket/v3/date
[DEBUG payload] 192.168.200.227:81 -> 192.168.200.34:55569 seq=683040071 len=502
  开头: HTTP/1.1 101 \r\nServer: nginx\r\nDate: Thu, 20 Aug 2026 01:52:48 GMT\r\nConnection: upgrade\r\nAccess-control-Allow-Origin: *\r\n

[DEBUG parse_http_messages] 找到 1 个HTTP消息:
  [0] 响应: 101
[DEBUG payload] 192.168.200.227:81 -> 192.168.200.34:62980 seq=2024181143 len=898
  开头: HTTP/1.1 200 \r\nServer: nginx\r\nDate: Thu, 20 Aug 2026 01:52:48 GMT\r\nContent-Type: application/json;charset=UTF-8\r\nTransfe

[DEBUG parse_http_messages] 找到 1 个HTTP消息:
  [0] 响应: 200

========================================================================================================================
📥 响应 [11:05:29.728]
========================================================================================================================
  状态码: 200 
  端点: serverGroupList
  来源: 192.168.200.227:81
  目标: 192.168.200.34:62980
  content-type: application/json;charset=UTF-8

  📦 响应JSON Body (完整):
    {
    "code": 200,
    "tips": "成功",
    "attribute": {},
    "data": [
        {
            "id": 1,
            "name": "自己"
        },
        {
            "id": 2,
            "name": "192.168.101.240_3306"
        },
        {
            "id": 3,
            "name": "192.168.200.127_40001"
        },
       
    ],
    "timestamp": 1787190768687,
    "success": true
}

  ⏱️  响应时间: 0.99ms
  🔗 对应请求: #4 GET serverGroupList
========================================================================================================================
..........................

省略了一部分

📤 请求 #6 [11:05:29.730]
========================================================================================================================
  方法: POST
  端点: riskList
  路径: /webserver/api/v3/risk/riskList
  来源: 192.168.200.34:62980
  目标: 192.168.200.227:81
  TCP Seq: 2089189153
  host: 192.168.200.227:81
  user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/150.0.0.0 Safari/537.36
  content-type: application/json;charset=UTF-8
  cookie: dbAudit.sid=b6ee336c-59fb-4635...

  📦 请求JSON Body:
    {
    "dbIdList": [],
    
    "page": {
        "current": 1,
        "pageSize": 20
    }
}
========================================================================================================================
[DEBUG payload] 192.168.200.227:81 -> 192.168.200.34:62980 seq=2024182851 len=762
  开头: HTTP/1.1 200 \r\nServer: nginx\r\nDate: Thu, 20 Aug 2026 01:52:49 GMT\r\nContent-Type: application/json;charset=UTF-8\r\nTransfe

[DEBUG parse_http_messages] 找到 1 个HTTP消息:
  [0] 响应: 200

========================================================================================================================
📥 响应 [11:05:29.730]
========================================================================================================================
  状态码: 200 
  端点: riskList
  来源: 192.168.200.227:81
  目标: 192.168.200.34:62980
  content-type: application/json;charset=UTF-8

  📦 响应JSON Body (完整):
    {
    "code": 500,
    "tips": "服务器错误",
    "attribute": {},
    "data": null,
    "timestamp": 1787190769170,
    "success": false
}

  ⏱️  响应时间: 0.64ms
  🔗 对应请求: #6 POST riskList
========================================================================================================================

[DEBUG payload] 192.168.200.227:81 -> 192.168.200.34:55569 seq=683040607 len=11
  开头:         )kjT

[DEBUG payload] 192.168.200.227:81 -> 192.168.200.34:54105 seq=262463728 len=16
  开头: VJ/+Q26

按ctrl +c

捕获已停止

========================================================================================================================

📊 API监控统计报告

========================================================================================================================

运行时间: 0:05:15.860342

API请求数: 6

📌 API端点统计:

isPlatform: 1 次

info: 1 次

config: 1 次

serverGroupList: 1 次

listDataBaseGroups: 1 次

riskList: 1 次

📌 响应状态码分布:

200: 5 次

🔄 请求-响应配对:

#1 POST isPlatform -> 200

#3 GET config -> 200

#4 GET serverGroupList -> 200

#5 POST listDataBaseGroups -> 200

#6 POST riskList -> 200

========================================================================================================================

抓任意个部署http服务的机器的包的命令,这个时候要登录那个机器执行这个

tcpdump -i ens192 -w /tmp/api_capture.pcap 'tcp port 81' -s 0

执行后,访问这个机器的网页,产生http流量,然后产生了一些,就在机器上执行ctrl+c停止抓包。

拷贝到192.168.200.228的机器,让这个机器分析网络包。

scp /tmp/api_capture.pcap root@192.168.200.228:/tmp/

看看有没有抓到包,下边显示的78就是抓到了78个包。

tcpdump -r /tmp/api_capture.pcap | wc -l

相关推荐
雷帝木木1 小时前
数据湖与数据仓库:从理论到实践
人工智能·python·深度学习·机器学习
caimouse2 小时前
ReactOS 图形系统分析(51):元文件子系统 — metafile.c
c语言·开发语言
数字化转型分享点滴2 小时前
机加工车间上线 SH‑AIOT 物联网会遇到哪些常见实施难点
python·物联网
0566462 小时前
agent学习——流式响应与文本切分
网络·python·学习
学习星球2 小时前
Solid.js 实战:拆解官方 RealWorld 项目
开发语言·javascript·vue.js
OPEN-F2 小时前
Python进阶教程:正则表达式进阶与文本处理
开发语言·python·正则表达式
新网企兴2 小时前
2026年陕西做GEO推广,找新网企兴解决获客难题
python·搜索引擎
zhifou1234563 小时前
java 17升级安装
java·开发语言
青 春 记 忆3 小时前
LeetCode 142. 环形链表 II|Python 解法详解
python·leetcode·链表