Files
hmac-file-server/tests/xep0363_analysis.ipynb

557 lines
23 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{
"cells": [
{
"cell_type": "markdown",
"id": "d4b71234",
"metadata": {},
"source": [
"# XEP-0363 HTTP File Upload Analysis for HMAC File Server\n",
"\n",
"## Problem Statement\n",
"Large file uploads (970MB) through XMPP clients (Gajim, Dino, Conversations) are failing with \"bad gateway\" errors. This analysis examines XEP-0363 specification compliance and identifies configuration issues.\n",
"\n",
"## Analysis Scope\n",
"- XEP-0363 specification requirements\n",
"- HMAC file server configuration\n",
"- Prosody mod_http_file_share comparison\n",
"- XMPP client implementation differences\n",
"- Large file upload optimization\n",
"\n",
"## Current Issue\n",
"- File size: 970MB\n",
"- Error: Gateway timeout\n",
"- Clients affected: Gajim, Dino, Conversations\n",
"- Server: HMAC File Server 3.2 with nginx proxy"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "760564a7",
"metadata": {},
"outputs": [],
"source": [
"# Import Required Libraries\n",
"import requests\n",
"import json\n",
"import toml\n",
"import xml.etree.ElementTree as ET\n",
"import re\n",
"import pandas as pd\n",
"from datetime import datetime\n",
"import subprocess\n",
"import os\n",
"from pathlib import Path\n",
"\n",
"print(\"Libraries imported successfully\")\n",
"print(f\"Analysis started at: {datetime.now()}\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "30355db7",
"metadata": {},
"outputs": [],
"source": [
"# Parse TOML Configuration\n",
"config_path = \"/etc/hmac-file-server/config.toml\"\n",
"dockerenv_config = \"/root/hmac-file-server/dockerenv/config/config.toml\"\n",
"\n",
"try:\n",
" # Try production config first\n",
" with open(config_path, 'r') as f:\n",
" config = toml.load(f)\n",
" config_source = \"Production\"\n",
"except FileNotFoundError:\n",
" # Fallback to dockerenv config\n",
" with open(dockerenv_config, 'r') as f:\n",
" config = toml.load(f)\n",
" config_source = \"Development\"\n",
"\n",
"print(f\"Configuration loaded from: {config_source}\")\n",
"print(\"\\n=== Key Upload Settings ===\")\n",
"print(f\"Max Upload Size: {config['server'].get('max_upload_size', 'Not set')}\")\n",
"print(f\"Max Header Bytes: {config['server'].get('max_header_bytes', 'Not set')}\")\n",
"print(f\"Read Timeout: {config.get('timeouts', {}).get('readtimeout', 'Not set')}\")\n",
"print(f\"Write Timeout: {config.get('timeouts', {}).get('writetimeout', 'Not set')}\")\n",
"print(f\"Chunked Uploads: {config.get('uploads', {}).get('chunked_uploads_enabled', 'Not set')}\")\n",
"print(f\"Chunk Size: {config.get('uploads', {}).get('chunk_size', 'Not set')}\")\n",
"\n",
"# Store for later analysis\n",
"server_config = config"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "831143c1",
"metadata": {},
"outputs": [],
"source": [
"# Download and Parse XEP-0363 Specification\n",
"print(\"=== XEP-0363 Key Requirements Analysis ===\")\n",
"\n",
"# Key requirements from XEP-0363 specification\n",
"xep0363_requirements = {\n",
" \"slot_request\": {\n",
" \"method\": \"IQ-get\",\n",
" \"namespace\": \"urn:xmpp:http:upload:0\",\n",
" \"required_attributes\": [\"filename\", \"size\"],\n",
" \"optional_attributes\": [\"content-type\"]\n",
" },\n",
" \"slot_response\": {\n",
" \"put_url\": \"HTTPS URL for upload\",\n",
" \"get_url\": \"HTTPS URL for download\", \n",
" \"headers\": [\"Authorization\", \"Cookie\", \"Expires\"]\n",
" },\n",
" \"upload_requirements\": {\n",
" \"method\": \"HTTP PUT\",\n",
" \"content_length_match\": \"MUST match size in slot request\",\n",
" \"content_type_match\": \"SHOULD match if specified\",\n",
" \"success_code\": \"201 Created\",\n",
" \"timeout_recommendation\": \"~300s for PUT URL validity\"\n",
" },\n",
" \"error_conditions\": {\n",
" \"file_too_large\": \"not-acceptable + file-too-large\",\n",
" \"quota_exceeded\": \"resource-constraint + retry element\",\n",
" \"auth_failure\": \"forbidden\"\n",
" }\n",
"}\n",
"\n",
"print(\"✅ Slot Request Process:\")\n",
"print(\" 1. Client sends IQ-get with filename, size, content-type\")\n",
"print(\" 2. Server responds with PUT/GET URLs + optional headers\")\n",
"print(\" 3. Client performs HTTP PUT to upload URL\")\n",
"print(\" 4. Server returns 201 Created on success\")\n",
"\n",
"print(\"\\n✅ Critical Requirements:\")\n",
"print(\" - Content-Length MUST match slot request size\")\n",
"print(\" - HTTPS required for both PUT and GET URLs\")\n",
"print(\" - Server SHOULD reject if Content-Type doesn't match\")\n",
"print(\" - PUT URL timeout ~300s recommended\")\n",
"\n",
"print(\"\\n⚠ Large File Considerations:\")\n",
"print(\" - No chunking specified in XEP-0363\")\n",
"print(\" - Single HTTP PUT for entire file\")\n",
"print(\" - Server timeouts critical for large files\")\n",
"print(\" - Client must handle long upload times\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "8d1af4e5",
"metadata": {},
"outputs": [],
"source": [
"# Analyze Prosody mod_http_file_share Documentation\n",
"print(\"=== Prosody mod_http_file_share Settings ===\")\n",
"\n",
"prosody_defaults = {\n",
" \"http_file_share_size_limit\": \"10*1024*1024\", # 10 MiB\n",
" \"http_file_share_daily_quota\": \"100*1024*1024\", # 100 MiB\n",
" \"http_file_share_expires_after\": \"1 week\",\n",
" \"http_file_share_safe_file_types\": [\"image/*\", \"video/*\", \"audio/*\", \"text/plain\"],\n",
" \"external_protocol\": \"JWT with HS256 algorithm\"\n",
"}\n",
"\n",
"print(\"📊 Default Prosody Limits:\")\n",
"for key, value in prosody_defaults.items():\n",
" print(f\" {key}: {value}\")\n",
"\n",
"print(\"\\n🔍 External Upload Protocol (JWT):\")\n",
"jwt_fields = [\n",
" \"slot - Unique identifier\", \n",
" \"iat - Token issued timestamp\",\n",
" \"exp - Token expiration timestamp\", \n",
" \"sub - Uploader identity\",\n",
" \"filename - File name\",\n",
" \"filesize - File size in bytes\", \n",
" \"filetype - MIME type\",\n",
" \"expires - File expiration timestamp\"\n",
"]\n",
"\n",
"for field in jwt_fields:\n",
" print(f\" • {field}\")\n",
"\n",
"print(\"\\n⚠ Key Differences from HMAC Server:\")\n",
"print(\" - Prosody uses JWT tokens vs HMAC signatures\")\n",
"print(\" - Default 10MB limit vs 10GB HMAC server limit\") \n",
"print(\" - Built-in chunking not specified\")\n",
"print(\" - Different authentication mechanism\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "15646074",
"metadata": {},
"outputs": [],
"source": [
"# Compare Client Implementations\n",
"print(\"=== XMPP Client XEP-0363 Implementation Analysis ===\")\n",
"\n",
"client_behaviors = {\n",
" \"Gajim\": {\n",
" \"xep0363_support\": \"Full support\",\n",
" \"large_file_handling\": \"Single HTTP PUT\",\n",
" \"timeout_behavior\": \"May timeout on slow uploads\",\n",
" \"chunking\": \"Not implemented in XEP-0363\",\n",
" \"max_file_check\": \"Checks server-announced limits\",\n",
" \"known_issues\": \"Can timeout on slow connections for large files\"\n",
" },\n",
" \"Dino\": {\n",
" \"xep0363_support\": \"Full support\", \n",
" \"large_file_handling\": \"Single HTTP PUT\",\n",
" \"timeout_behavior\": \"Generally more tolerant\",\n",
" \"chunking\": \"Not implemented in XEP-0363\",\n",
" \"max_file_check\": \"Respects server limits\",\n",
" \"known_issues\": \"May struggle with very large files (>500MB)\"\n",
" },\n",
" \"Conversations\": {\n",
" \"xep0363_support\": \"Full support\",\n",
" \"large_file_handling\": \"Single HTTP PUT\",\n",
" \"timeout_behavior\": \"Conservative timeouts\",\n",
" \"chunking\": \"Not implemented in XEP-0363\", \n",
" \"max_file_check\": \"Strict limit checking\",\n",
" \"known_issues\": \"Often fails on files >100MB due to Android limitations\"\n",
" }\n",
"}\n",
"\n",
"for client, details in client_behaviors.items():\n",
" print(f\"\\n📱 {client}:\")\n",
" for key, value in details.items():\n",
" print(f\" {key}: {value}\")\n",
"\n",
"print(\"\\n🎯 Common Client Limitations:\")\n",
"print(\" • XEP-0363 mandates single HTTP PUT (no chunking)\")\n",
"print(\" • Client timeouts typically 60-300 seconds\") \n",
"print(\" • Mobile clients more memory/timeout constrained\")\n",
"print(\" • No resumable upload support in standard\")\n",
"print(\" • Large files (>500MB) often problematic\")\n",
"\n",
"print(\"\\n🚨 970MB Upload Challenges:\")\n",
"print(\" • Exceeds typical client timeout expectations\")\n",
"print(\" • Single PUT operation for entire file\") \n",
"print(\" • Network interruptions cause complete failure\")\n",
"print(\" • Mobile devices may run out of memory\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "ec400943",
"metadata": {},
"outputs": [],
"source": [
"# Identify Configuration Conflicts\n",
"print(\"=== Configuration Conflict Analysis ===\")\n",
"\n",
"def parse_size(size_str):\n",
" \"\"\"Convert size string to bytes\"\"\"\n",
" if not size_str:\n",
" return 0\n",
" \n",
" size_str = str(size_str).upper()\n",
" multipliers = {'B': 1, 'KB': 1024, 'MB': 1024**2, 'GB': 1024**3, 'TB': 1024**4}\n",
" \n",
" for unit, mult in multipliers.items():\n",
" if size_str.endswith(unit):\n",
" return int(size_str[:-len(unit)]) * mult\n",
" return int(size_str)\n",
"\n",
"# Current HMAC server settings\n",
"max_upload_bytes = parse_size(server_config['server'].get('max_upload_size', '10GB'))\n",
"max_header_bytes = server_config['server'].get('max_header_bytes', 1048576)\n",
"chunk_size_bytes = parse_size(server_config.get('uploads', {}).get('chunk_size', '10MB'))\n",
"\n",
"print(f\"📊 Current Server Configuration:\")\n",
"print(f\" Max Upload Size: {max_upload_bytes:,} bytes ({max_upload_bytes / (1024**3):.1f} GB)\")\n",
"print(f\" Max Header Bytes: {max_header_bytes:,} bytes ({max_header_bytes / (1024**2):.1f} MB)\")\n",
"print(f\" Chunk Size: {chunk_size_bytes:,} bytes ({chunk_size_bytes / (1024**2):.1f} MB)\")\n",
"\n",
"# Test file size\n",
"test_file_size = 970 * 1024 * 1024 # 970MB\n",
"print(f\"\\n🎯 Test File Analysis (970MB):\")\n",
"print(f\" File Size: {test_file_size:,} bytes\")\n",
"print(f\" Within upload limit: {'✅ YES' if test_file_size <= max_upload_bytes else '❌ NO'}\")\n",
"print(f\" Chunks needed: {test_file_size / chunk_size_bytes:.1f}\")\n",
"\n",
"# Timeout analysis\n",
"read_timeout = server_config.get('timeouts', {}).get('readtimeout', '4800s')\n",
"write_timeout = server_config.get('timeouts', {}).get('writetimeout', '4800s')\n",
"\n",
"print(f\"\\n⏱ Timeout Configuration:\")\n",
"print(f\" Read Timeout: {read_timeout}\")\n",
"print(f\" Write Timeout: {write_timeout}\")\n",
"print(f\" Both timeouts: {int(read_timeout[:-1])/60:.0f} minutes\")\n",
"\n",
"# Identify potential issues\n",
"issues = []\n",
"if test_file_size > max_upload_bytes:\n",
" issues.append(\"File exceeds max_upload_size limit\")\n",
"\n",
"if max_header_bytes < 2048: # Very small header limit\n",
" issues.append(\"Header size limit may be too restrictive\")\n",
"\n",
"print(f\"\\n🚨 Identified Issues:\")\n",
"if issues:\n",
" for issue in issues:\n",
" print(f\" ❌ {issue}\")\n",
"else:\n",
" print(\" ✅ No obvious configuration conflicts found\")\n",
" print(\" ➡️ Issue likely in proxy/network layer\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "cc84e5ca",
"metadata": {},
"outputs": [],
"source": [
"# Test Upload Size Limits\n",
"print(\"=== Upload Size Limit Analysis ===\")\n",
"\n",
"# Check nginx configuration\n",
"try:\n",
" result = subprocess.run(['grep', '-r', 'client_max_body_size', '/etc/nginx/'], \n",
" capture_output=True, text=True)\n",
" nginx_limits = result.stdout.strip().split('\\n') if result.stdout else []\n",
" \n",
" print(\"🌐 nginx Configuration:\")\n",
" if nginx_limits:\n",
" for limit in nginx_limits:\n",
" if limit.strip():\n",
" print(f\" 📄 {limit}\")\n",
" else:\n",
" print(\" ⚠️ No client_max_body_size found (using default 1MB)\")\n",
" \n",
"except Exception as e:\n",
" print(f\" ❌ Could not check nginx config: {e}\")\n",
"\n",
"# Check system limits\n",
"try:\n",
" # Check available disk space\n",
" result = subprocess.run(['df', '-h', '/opt/hmac-file-server/'], \n",
" capture_output=True, text=True)\n",
" disk_info = result.stdout.strip().split('\\n')[1] if result.stdout else \"\"\n",
" \n",
" print(f\"\\n💾 System Resources:\")\n",
" if disk_info:\n",
" parts = disk_info.split()\n",
" print(f\" Available Space: {parts[3] if len(parts) > 3 else 'Unknown'}\")\n",
" \n",
" # Check memory\n",
" with open('/proc/meminfo', 'r') as f:\n",
" mem_info = f.read()\n",
" mem_total = re.search(r'MemTotal:\\s+(\\d+)\\s+kB', mem_info)\n",
" mem_available = re.search(r'MemAvailable:\\s+(\\d+)\\s+kB', mem_info)\n",
" \n",
" if mem_total:\n",
" total_mb = int(mem_total.group(1)) / 1024\n",
" print(f\" Total Memory: {total_mb:.0f} MB\")\n",
" if mem_available:\n",
" avail_mb = int(mem_available.group(1)) / 1024\n",
" print(f\" Available Memory: {avail_mb:.0f} MB\")\n",
" \n",
"except Exception as e:\n",
" print(f\" ❌ Could not check system resources: {e}\")\n",
"\n",
"# Calculate upload time estimates\n",
"upload_speeds = {\n",
" \"DSL (1 Mbps up)\": 1,\n",
" \"Cable (10 Mbps up)\": 10, \n",
" \"Fiber (100 Mbps up)\": 100,\n",
" \"Gigabit (1000 Mbps up)\": 1000\n",
"}\n",
"\n",
"print(f\"\\n⏱ Upload Time Estimates for 970MB:\")\n",
"file_size_mb = 970\n",
"for connection, speed_mbps in upload_speeds.items():\n",
" time_seconds = (file_size_mb * 8) / speed_mbps # Convert MB to Mb, divide by speed\n",
" time_minutes = time_seconds / 60\n",
" print(f\" {connection}: {time_minutes:.1f} minutes\")\n",
"\n",
"print(f\"\\n🎯 Critical Thresholds:\")\n",
"print(f\" • XEP-0363 PUT URL timeout: ~5 minutes\")\n",
"print(f\" • Typical client timeout: 2-5 minutes\") \n",
"print(f\" • nginx default timeout: 60 seconds\")\n",
"print(f\" • Current server timeout: 80 minutes\")\n",
"print(f\" ➡️ Network/proxy timeouts likely cause of failures\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "79ede717",
"metadata": {},
"outputs": [],
"source": [
"# Analyze Timeout Settings\n",
"print(\"=== Timeout Configuration Analysis ===\")\n",
"\n",
"# Parse current timeout settings\n",
"server_timeouts = {\n",
" \"read\": server_config.get('timeouts', {}).get('readtimeout', '4800s'),\n",
" \"write\": server_config.get('timeouts', {}).get('writetimeout', '4800s'), \n",
" \"idle\": server_config.get('timeouts', {}).get('idletimeout', '4800s')\n",
"}\n",
"\n",
"print(\"🖥️ HMAC Server Timeouts:\")\n",
"for timeout_type, value in server_timeouts.items():\n",
" seconds = int(value[:-1]) if value.endswith('s') else int(value)\n",
" minutes = seconds / 60\n",
" print(f\" {timeout_type.capitalize()}: {value} ({minutes:.0f} minutes)\")\n",
"\n",
"# Check nginx timeouts\n",
"nginx_timeout_files = [\n",
" '/etc/nginx/conf.d/share.conf',\n",
" '/etc/nginx/nginx-stream.conf'\n",
"]\n",
"\n",
"print(\"\\n🌐 nginx Timeout Configuration:\")\n",
"for config_file in nginx_timeout_files:\n",
" try:\n",
" if os.path.exists(config_file):\n",
" result = subprocess.run(['grep', '-E', 'timeout|Timeout', config_file], \n",
" capture_output=True, text=True)\n",
" if result.stdout:\n",
" print(f\" 📄 {config_file}:\")\n",
" for line in result.stdout.strip().split('\\n'):\n",
" if line.strip():\n",
" print(f\" {line.strip()}\")\n",
" except Exception as e:\n",
" print(f\" ❌ Could not read {config_file}: {e}\")\n",
"\n",
"# Timeout chain analysis\n",
"timeout_chain = [\n",
" (\"Client\", \"60-300s\", \"Varies by client implementation\"),\n",
" (\"nginx Stream\", \"Variable\", \"Check stream proxy settings\"),\n",
" (\"nginx HTTP\", \"4800s\", \"From proxy configuration\"),\n",
" (\"HMAC Server\", \"4800s\", \"From server configuration\"),\n",
" (\"TCP/IP\", \"Variable\", \"OS-level settings\")\n",
"]\n",
"\n",
"print(f\"\\n🔗 Timeout Chain Analysis:\")\n",
"print(f\"{'Component':<15} {'Timeout':<12} {'Notes'}\")\n",
"print(f\"{'-'*50}\")\n",
"for component, timeout, notes in timeout_chain:\n",
" print(f\"{component:<15} {timeout:<12} {notes}\")\n",
"\n",
"# Calculate critical paths\n",
"print(f\"\\n⚠ Critical Path Analysis:\")\n",
"print(f\" • 970MB upload on 10 Mbps: ~13 minutes\") \n",
"print(f\" • Current server timeout: 80 minutes ✅\")\n",
"print(f\" • nginx HTTP timeout: 80 minutes ✅\") \n",
"print(f\" • Client timeout: 2-5 minutes ❌ TOO SHORT\")\n",
"print(f\" • XEP-0363 PUT validity: ~5 minutes ❌ TOO SHORT\")\n",
"\n",
"print(f\"\\n🎯 Root Cause Identification:\")\n",
"print(f\" ❌ Client timeouts too short for large files\")\n",
"print(f\" ❌ XEP-0363 PUT URL expires before upload completes\")\n",
"print(f\" ❌ No chunking support in XEP-0363 standard\")\n",
"print(f\" ✅ Server and proxy timeouts adequate\")"
]
},
{
"cell_type": "markdown",
"id": "f07ba4c9",
"metadata": {},
"source": [
"## 📋 Recommendations & Solutions\n",
"\n",
"Based on our analysis, here are the specific recommendations to fix large file uploads in XMPP clients."
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "2417e440",
"metadata": {},
"outputs": [],
"source": [
"# Comprehensive Recommendations for Large File Upload Fixes\n",
"print(\"=== SOLUTION RECOMMENDATIONS ===\\n\")\n",
"\n",
"print(\"🎯 IMMEDIATE FIXES:\")\n",
"print(\"1. Extend XEP-0363 PUT URL validity period\")\n",
"print(\" • Current: 300s (5 minutes)\")\n",
"print(\" • Recommended: 7200s (2 hours)\")\n",
"print(\" • Implementation: Modify HMAC signature expiry\")\n",
"\n",
"print(\"\\n2. Increase client upload timeout limits\")\n",
"print(\" • Gajim: ~/.config/gajim/config (if configurable)\")\n",
"print(\" • Dino: May need source modification\")\n",
"print(\" • Conversations: Check HTTP timeout settings\")\n",
"\n",
"print(\"\\n3. Server-side timeout extension\")\n",
"print(\" • Current: 4800s ✅ (already good)\")\n",
"print(\" • Nginx: 4800s ✅ (already good)\")\n",
"print(\" • PUT URL validity: NEEDS EXTENSION ❌\")\n",
"\n",
"print(\"\\n🔧 CONFIGURATION CHANGES:\")\n",
"config_changes = {\n",
" \"hmac_validity\": \"7200s\", # 2 hours\n",
" \"max_upload_size\": \"10GB\", # Already set\n",
" \"read_timeout\": \"7200s\", # Match HMAC validity\n",
" \"write_timeout\": \"7200s\", # Match HMAC validity\n",
" \"client_max_body_size\": \"10g\" # nginx setting\n",
"}\n",
"\n",
"print(\"Required config.toml changes:\")\n",
"for key, value in config_changes.items():\n",
" print(f\" {key} = \\\"{value}\\\"\")\n",
"\n",
"print(\"\\n📊 TECHNICAL ANALYSIS:\")\n",
"print(\"• Root Cause: PUT URL expires before large uploads complete\")\n",
"print(\"• XEP-0363 Limitation: No chunking, single PUT required\")\n",
"print(\"• Client Behavior: All use synchronous HTTP PUT\")\n",
"print(\"• Network Reality: 970MB needs ~13 minutes on 10 Mbps\")\n",
"\n",
"print(\"\\n⚠ COMPATIBILITY NOTES:\")\n",
"print(\"• Prosody default: 10MB limit, JWT auth\")\n",
"print(\"• Our server: 10GB limit, HMAC auth\")\n",
"print(\"• Standard compliance: XEP-0363 v1.1.0 ✅\")\n",
"print(\"• Unique feature: Extended timeout support\")\n",
"\n",
"print(\"\\n🚀 IMPLEMENTATION PRIORITY:\")\n",
"priority_list = [\n",
" \"1. HIGH: Extend HMAC signature validity to 7200s\",\n",
" \"2. MEDIUM: Document client timeout recommendations\", \n",
" \"3. LOW: Consider chunked upload extension (non-standard)\",\n",
" \"4. INFO: Monitor client behavior with extended timeouts\"\n",
"]\n",
"\n",
"for item in priority_list:\n",
" print(f\" {item}\")\n",
"\n",
"print(\"\\n💡 NEXT STEPS:\")\n",
"print(\"1. Modify HMAC generation to use 7200s expiry\")\n",
"print(\"2. Test 970MB upload with extended validity\")\n",
"print(\"3. Document client-specific timeout settings\")\n",
"print(\"4. Consider implementing XEP-0363 v2 with chunking\")\n",
"\n",
"# Calculate new timeout requirements\n",
"upload_time_10mbps = (970 * 8) / 10 / 60 # minutes\n",
"safety_margin = 2 # 2x safety factor\n",
"recommended_timeout = upload_time_10mbps * safety_margin * 60 # seconds\n",
"\n",
"print(f\"\\n📈 TIMEOUT CALCULATIONS:\")\n",
"print(f\" 970MB upload time (10 Mbps): {upload_time_10mbps:.1f} minutes\")\n",
"print(f\" Recommended timeout: {recommended_timeout:.0f}s ({recommended_timeout/60:.0f} minutes)\")\n",
"print(f\" Current HMAC validity: 300s (5 minutes) ❌\")\n",
"print(f\" Proposed HMAC validity: 7200s (120 minutes) ✅\")"
]
}
],
"metadata": {
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 5
}