httpcan 0.5.4

A simple HTTP Request & Response Service, with httpbin compatibility
Documentation
#!/usr/bin/env python3
"""
正确实现 auth-int 的测试客户端
手动计算 Digest Auth 以验证服务器实现
"""

import hashlib
import requests
from urllib.parse import urlparse

def md5(s):
    return hashlib.md5(s.encode() if isinstance(s, str) else s).hexdigest()

def test_server_auth_int():
    url = "http://localhost:8080/digest-auth/auth-int/testuser/testpass"
    body = '{"test":"data"}'
    
    print("="*70)
    print("手动实现 auth-int Digest Auth 测试")
    print("="*70)
    print()
    
    # Step 1: 获取 challenge
    print("Step 1: 发送初始请求获取 401 challenge...")
    response = requests.post(url, data=body)
    
    if response.status_code != 401:
        print(f"✗ 期望 401,得到 {response.status_code}")
        return False
    
    # 解析 WWW-Authenticate header
    auth_header = response.headers.get('WWW-Authenticate', '')
    print(f"收到 challenge: {auth_header}")
    print()
    
    # 提取参数
    import re
    realm_match = re.search(r'realm="([^"]+)"', auth_header)
    nonce_match = re.search(r'nonce="([^"]+)"', auth_header)
    opaque_match = re.search(r'opaque="([^"]+)"', auth_header)
    qop_match = re.search(r'qop="([^"]+)"', auth_header)
    
    if not all([realm_match, nonce_match, qop_match]):
        print("✗ 无法解析 challenge")
        return False
    
    realm = realm_match.group(1)
    nonce = nonce_match.group(1)
    opaque = opaque_match.group(1) if opaque_match else ""
    qop = qop_match.group(1)
    
    print(f"  realm:  {realm}")
    print(f"  nonce:  {nonce}")
    print(f"  opaque: {opaque}")
    print(f"  qop:    {qop}")
    print()
    
    # Step 2: 按照 RFC-7616 计算响应
    print("Step 2: 按 RFC-7616 auth-int 规范计算响应...")
    
    username = "testuser"
    password = "testpass"
    method = "POST"
    uri = urlparse(url).path
    nc = "00000001"
    cnonce = "0a4f113b"  # 客户端随机数
    
    # 计算 HA1
    ha1 = md5(f"{username}:{realm}:{password}")
    print(f"  HA1 = MD5({username}:{realm}:{password})")
    print(f"      = {ha1}")
    
    # 计算 body hash
    body_hash = md5(body)
    print(f"  Body Hash = MD5({repr(body)})")
    print(f"            = {body_hash}")
    
    # 计算 HA2 (auth-int 方式)
    ha2 = md5(f"{method}:{uri}:{body_hash}")
    print(f"  HA2 = MD5({method}:{uri}:{body_hash})")
    print(f"      = {ha2}")
    
    # 计算最终响应
    response_hash = md5(f"{ha1}:{nonce}:{nc}:{cnonce}:{qop}:{ha2}")
    print(f"  Response = MD5({ha1}:{nonce}:{nc}:{cnonce}:{qop}:{ha2})")
    print(f"           = {response_hash}")
    print()
    
    # Step 3: 发送带认证的请求
    print("Step 3: 发送带正确 auth-int 认证的请求...")
    
    auth_response = (
        f'Digest username="{username}", '
        f'realm="{realm}", '
        f'nonce="{nonce}", '
        f'uri="{uri}", '
        f'qop={qop}, '
        f'nc={nc}, '
        f'cnonce="{cnonce}", '
        f'response="{response_hash}", '
        f'opaque="{opaque}"'
    )
    
    print(f"  Authorization: {auth_response[:80]}...")
    print()
    
    response = requests.post(
        url,
        data=body,
        headers={
            'Authorization': auth_response,
            'Content-Type': 'application/json'
        }
    )
    
    print(f"响应状态: {response.status_code}")
    print(f"响应内容: {response.text}")
    print()
    
    if response.status_code == 200 and '"authenticated":true' in response.text:
        print("="*70)
        print("✅ 成功!服务器正确实现了 RFC-7616 auth-int QoP")
        print("="*70)
        print()
        print("服务器验证了:")
        print("  1. HA1 = MD5(username:realm:password)")
        print("  2. HA2 = MD5(method:uri:MD5(body))  ← 包含 body hash")
        print("  3. Response = MD5(HA1:nonce:nc:cnonce:qop:HA2)")
        print()
        print("这证明修复是成功的!")
        return True
    else:
        print("✗ 认证失败")
        return False

if __name__ == "__main__":
    success = test_server_auth_int()
    exit(0 if success else 1)