From 2e499444c09c35f0845cb93d48e4cdbb75edcc73 Mon Sep 17 00:00:00 2001 From: ThinhHV Date: Fri, 1 May 2026 04:01:43 +0000 Subject: [PATCH] feat(default-source-url): Add default source URL --- .env.sample | 233 ++++++++++---- Cargo.lock | 554 ++++++++++++++++++-------------- src/common/config/loader.rs | 44 +++ src/modules/cli/args.rs | 96 +++--- src/modules/proxy/controller.rs | 144 +++++++-- src/modules/proxy/dto/params.rs | 62 +++- src/modules/proxy/fallback.rs | 3 +- src/modules/proxy/service.rs | 235 +++++++++++++- 8 files changed, 987 insertions(+), 384 deletions(-) diff --git a/.env.sample b/.env.sample index d7e4445..875e4ad 100644 --- a/.env.sample +++ b/.env.sample @@ -1,39 +1,71 @@ +# ============================================================ +# PreviewProxy - Environment Configuration +# ============================================================ +# +# Copy this file to .env and fill in values for your deployment. +# All variables are optional and commented out; built-in defaults +# are shown after the "=" sign in each comment. +# +# ============================================================ + # ============================================================ # App / Server # ============================================================ -# Runtime environment. Options: development, production +# Runtime environment. Affects logging defaults and error verbosity. +# Options: development, production # PP_APP_ENV=development -# HTTP port the server listens on +# HTTP port the server listens on. # PP_PORT=8080 -# Max in-flight requests before returning 503 +# Max in-flight requests before returning 503 Service Unavailable. +# Tune to match your CPU core count x expected concurrency. # PP_MAX_CONCURRENT_REQUESTS=256 -# General response TTL (seconds) +# Default Cache-Control max-age (seconds) for successfully processed responses. +# 86400 = 1 day. Override per-request with the ?ttl= query param. # PP_TTL=86400 -# Log level filter (see https://docs.rs/tracing-subscriber) +# Log level filter. Use "previewproxy=debug" during development. +# Production recommendation: previewproxy=info,tower_http=warn +# See: https://docs.rs/tracing-subscriber # RUST_LOG=previewproxy=info,tower_http=info # ============================================================ # Security # ============================================================ -# Secret key used to sign/verify request URLs. Leave empty to disable signing. +# Secret key (any length, treated as raw bytes) used to sign and verify +# request URLs with HMAC-SHA256. Prevents unauthorized use of the proxy. +# Leave empty to disable URL signing (open proxy - only safe on private networks). +# Generate: openssl rand -hex 32 # PP_HMAC_KEY= -# Comma-separated list of upstream hostnames allowed as image sources. Leave empty to allow all. +# Allowlist of upstream hostnames that may be used as image sources. +# Protects against SSRF by rejecting requests to unlisted hosts. +# Leave empty to allow any host (not recommended in production). +# Supports exact matches and single-label wildcards: +# images.example.com - exact match only +# *.example.com - matches www.example.com but NOT a.b.example.com or example.com +# Example: PP_ALLOWED_HOSTS=images.example.com,*.cdn.example.com,s3.amazonaws.com # PP_ALLOWED_HOSTS= -# Value for the Access-Control-Allow-Origin header. Use * to allow all. +# Comma-separated list of allowed CORS origins. +# Supports three modes: +# * - allow any origin +# *.example.com - single-label wildcard (matches sub.example.com, not a.b.example.com) +# https://app.example.com - exact origin match (scheme + host + optional port) +# If any entry is *, all origins are allowed regardless of other entries. +# Example: PP_CORS_ALLOW_ORIGIN=https://app.example.com,*.staging.example.com # PP_CORS_ALLOW_ORIGIN=* -# How long (seconds) the browser may cache CORS preflight responses. +# How long (seconds) the browser may cache CORS preflight (OPTIONS) responses. # PP_CORS_MAX_AGE_SECS=600 -# Hex-encoded AES key for encrypting/decrypting source URLs. +# Hex-encoded AES key for encrypting/decrypting source URLs passed to the proxy. +# Clients encrypt the source URL; the proxy decrypts it before fetching. +# Useful to prevent leaking upstream URLs in browser history or logs. # 32 hex chars = AES-128, 48 = AES-192, 64 = AES-256. Leave empty to disable. # Generate: openssl rand -hex 32 # PP_SOURCE_URL_ENCRYPTION_KEY= @@ -42,46 +74,59 @@ # Upstream Fetching # ============================================================ -# Max seconds to wait for an upstream HTTP response before aborting. +# Max seconds to wait for upstream to start sending response headers. +# Increase for slow origins; decrease for aggressive SLA enforcement. # PP_FETCH_TIMEOUT_SECS=10 -# Number of times to retry a fetch on connection errors (timeout, reset, broken pipe). -# Set to 0 to disable retries. +# Number of additional attempts after the first failure on transient errors +# (connection timeout, reset, broken pipe). Set to 0 to disable retries. # PP_FETCH_RETRY_COUNT=3 -# Delay in milliseconds between retry attempts. Set to 0 for immediate retry. +# Delay in milliseconds between retry attempts. +# 0 = immediate retry. Increase (e.g. 500) to back off on flaky upstreams. # PP_FETCH_RETRY_DELAY_MS=0 -# Max size (bytes) of a source file accepted for processing. Default: 20 MB. +# Maximum accepted size (bytes) of a source file. Requests exceeding this are +# rejected with 413. Default: 20 MB (20971520). Increase for video sources. # PP_MAX_SOURCE_BYTES=20971520 # ============================================================ # Cache # ============================================================ -# Max size (MB) of the in-memory (L1) cache. +# --- L1: In-memory cache (hot, fast, lost on restart) --- + +# Max total size (MB) of the in-memory LRU cache. # PP_CACHE_MEMORY_MAX_MB=256 -# Time-to-live (seconds) for entries in the in-memory cache. +# How long (seconds) entries live in the in-memory cache before expiry. +# 3600 = 1 hour # PP_CACHE_MEMORY_TTL_SECS=3600 -# Directory used for the disk (L2) cache. +# --- L2: Disk cache (warm, persistent across restarts) --- + +# Directory used for the disk cache. Must be writable by the process. +# Use a fast local SSD path in production (e.g. /var/cache/previewproxy). # PP_CACHE_DIR=/tmp/previewproxy -# Time-to-live (seconds) for entries on disk. +# How long (seconds) entries live on disk before eviction. +# 86400 = 1 day # PP_CACHE_DISK_TTL_SECS=86400 # Max total size (MB) of the disk cache. Leave empty for unlimited. +# Example: PP_CACHE_DISK_MAX_MB=10240 (10 GB) # PP_CACHE_DISK_MAX_MB= -# How often (seconds) the background cleanup task runs. +# How often (seconds) the background cleanup task scans and evicts stale entries. +# Lower values keep disk usage tighter; higher values reduce I/O overhead. # PP_CACHE_CLEANUP_INTERVAL_SECS=600 # ============================================================ -# Sources - S3-compatible (AWS, Cloudflare R2, etc.) +# Sources - S3-compatible (AWS, Cloudflare R2, RustFS, etc.) # ============================================================ -# Set to true to enable S3 as an image source. +# Enable S3 as an image source. When enabled, use the s3:/ scheme in URLs. +# Example request: //s3:/path/to/image.jpg # PP_S3_ENABLED=false # Name of the S3 bucket to read from. @@ -90,111 +135,183 @@ # AWS region (or equivalent) of the bucket. # PP_S3_REGION=us-east-1 -# Credentials for S3 authentication. +# IAM credentials with s3:GetObject permission on the bucket. # PP_S3_ACCESS_KEY_ID= # PP_S3_SECRET_ACCESS_KEY= -# Custom endpoint URL for S3-compatible services (e.g. Backblaze B2, Cloudflare R2,...). -# Leave empty for AWS. +# Custom endpoint URL for S3-compatible services. +# Leave empty to use standard AWS endpoints. +# Cloudflare R2: https://.r2.cloudflarestorage.com +# Backblaze B2: https://s3..backblazeb2.com # PP_S3_ENDPOINT= # ============================================================ # Sources - Local Filesystem # ============================================================ -# Set to true to enable serving images from the local filesystem. +# Enable serving images from the local filesystem. +# Useful for on-premise deployments or when assets are already on disk. +# Example request: //local:/relative/path/to/image.jpg # PP_LOCAL_ENABLED=false # Absolute path to the root directory for local image files. +# Requests using the local:/ scheme are resolved relative to this directory. +# Example: PP_LOCAL_BASE_DIR=/mnt/assets # PP_LOCAL_BASE_DIR= # ============================================================ # Sources - URL Aliases # ============================================================ -# Comma-separated alias=hostname mappings to rewrite short aliases in request URLs. -# Example: "cdn=https://cdn.example.com,assets=http://assets.example.com" +# Comma-separated alias=URL mappings. Clients can use a short alias instead +# of the full upstream URL, which hides upstream hostnames and shortens URLs. +# Alias keys are matched as a URL scheme prefix in the request path. +# +# Example: PP_URL_ALIASES=cdn=https://cdn.example.com,media=https://media.example.com +# Request //cdn:/images/photo.jpg +# -> fetches https://cdn.example.com/images/photo.jpg +# # PP_URL_ALIASES= +# ============================================================ +# Sources - Default Source URL +# ============================================================ + +# Base URL prepended to bare paths (no scheme) in requests. +# Lets clients send //path/to/image.jpg instead of a full URL, +# which simplifies client-side code and hides upstream details. +# +# Supports http://, https://, s3:/, local:/ schemes. +# +# Examples: +# PP_DEFAULT_SOURCE_URL=https://cdn.example.com +# PP_DEFAULT_SOURCE_URL=s3:/my-bucket +# PP_DEFAULT_SOURCE_URL=local:/var/assets +# +# PP_DEFAULT_SOURCE_URL= + # ============================================================ # Video (ffmpeg) # ============================================================ -# Path to the ffmpeg binary. +# Path to the ffmpeg binary. Must be in PATH or provide an absolute path. +# Required for video thumbnail extraction and format conversion. # PP_FFMPEG_PATH=ffmpeg -# Path to the ffprobe binary (defaults to ffprobe in the same directory as ffmpeg). -# PP_FFPROBE_PATH= +# Path to the ffprobe binary. Defaults to the same directory as ffmpeg. +# Leave empty to auto-detect alongside ffmpeg. +# PP_FFPROBE_PATH=ffprobe # ============================================================ # Best Format # ============================================================ +# "Best format" mode tries multiple output formats and picks the smallest +# that still meets quality requirements. Trigger with ?format=best or +# enable it for all requests with PP_BEST_FORMAT_BY_DEFAULT=true. +# ============================================================ -# Edge density threshold (0-100) for complexity classification. Images below this -# are treated as simple (lossless PNG included as a candidate). Default: 5.5 +# Edge density threshold (0-100) for image complexity classification. +# Images below this are considered simple (flat graphics, logos) and PNG is +# included as a lossless candidate. Images at or above are treated as +# complex (photos) and lossless formats are skipped. +# Default: 5.5 # PP_BEST_FORMAT_COMPLEXITY_THRESHOLD=5.5 -# When set, images with resolution above this value (in megapixels) skip the -# multi-format trial and pick one format directly. Leave empty to always trial all. +# Resolution (megapixels) above which the multi-format trial is skipped and +# a single format is chosen directly (faster for large images). +# Leave empty to always run the full trial regardless of size. +# Example: PP_BEST_FORMAT_MAX_RESOLUTION=25 (skip trial for images > 25 MP) # PP_BEST_FORMAT_MAX_RESOLUTION= -# When true, apply best-format selection for all requests that don't specify a format. +# Apply best-format selection for all requests that omit an explicit format. +# Equivalent to appending ?format=best to every request. # PP_BEST_FORMAT_BY_DEFAULT=false -# When true, skip re-encoding if the selected best format matches the source format -# and no other transforms are applied. +# Skip re-encoding if the winning format matches the source format and no +# other transforms (resize, crop, etc.) are requested. Saves CPU at the cost +# of not verifying the source is optimally encoded. # PP_BEST_FORMAT_ALLOW_SKIPS=false -# Comma-separated list of formats to trial when format=best is used. -# Only formats in this list are considered as candidates. Lossless formats (png, bmp, tiff) -# are automatically excluded for complex (high-edge-density) images regardless. -# Add avif or jxl for better compression at the cost of slower encoding. +# Ordered comma-separated list of formats to evaluate during the trial. +# Only listed formats are considered. Lossless formats (png, bmp, tiff) are +# automatically excluded for complex images regardless of this list. +# Add avif or jxl for better compression (slower encoding). +# Example: PP_BEST_FORMAT_PREFERRED_FORMATS=avif,webp,jpeg # PP_BEST_FORMAT_PREFERRED_FORMATS=jpeg,webp,png # ============================================================ # Disallow Lists # ============================================================ +# Block specific input types, transform operations, or output formats. +# Values are comma-separated tokens from the fixed lists below. +# Unknown tokens are silently ignored with a warning in the log. +# ============================================================ -# Blocks matching input source URLs (comma-separated regex patterns). +# Block source files by input format. +# Valid tokens: jpeg, png, gif, webp, avif, jxl, bmp, tiff, pdf, psd, video +# Example (block video and PDF inputs): +# PP_INPUT_DISALLOW_LIST=video,pdf # PP_INPUT_DISALLOW_LIST= -# Blocks matching transform operation names (comma-separated regex patterns). +# Block transform operations by name. +# Valid tokens: resize, rotate, flip, grayscale, brightness, contrast, blur, watermark, gif_anim +# Example (disable watermarking and animated GIF output): +# PP_TRANSFORM_DISALLOW_LIST=watermark,gif_anim # PP_TRANSFORM_DISALLOW_LIST= -# Blocks matching output format names (comma-separated regex patterns). +# Block output formats by name. +# Valid tokens: jpeg, png, gif, webp, avif, jxl, bmp, tiff, ico +# Example (ban uncompressed formats): +# PP_OUTPUT_DISALLOW_LIST=bmp,tiff # PP_OUTPUT_DISALLOW_LIST= # ============================================================ # Fallback Image # ============================================================ - -# Image served when an upstream fetch fails (404, timeout, too many redirects). -# Only one source should be set; priority if multiple are set: data > path > url. - -# Base64-encoded image data. Generate with: base64 fallback.png | tr -d '\n' +# +# Image returned when an upstream fetch results in: 404 not found, timeout, +# or too many redirects. Content type is auto-detected from +# the image magic bytes regardless of which source is used. +# If multiple sources are set, priority is: data > path > url. + +# Standard base64-encoded image data embedded directly in the config. +# No filesystem or network access needed at startup. +# Must be standard base64 (not URL-safe). Panics on startup if invalid. +# Generate: base64 -w 0 fallback.png (Linux) +# base64 -i fallback.png | tr -d '\n' (macOS) # PP_FALLBACK_IMAGE_DATA= -# Path to a locally stored fallback image file. +# Absolute path to a fallback image file on the local filesystem. +# Read from disk on each use (not cached). File must exist at startup. +# Example: PP_FALLBACK_IMAGE_PATH=/etc/previewproxy/fallback.png # PP_FALLBACK_IMAGE_PATH= -# URL of the fallback image (fetched once at startup). +# URL of the fallback image. Fetched once at startup and held in memory. +# The process will exit on startup if the URL is unreachable. +# Example: PP_FALLBACK_IMAGE_URL=https://cdn.example.com/fallback.png # PP_FALLBACK_IMAGE_URL= -# HTTP status code to use for fallback responses. Set to 0 to use the original -# error's status code instead. Default: 200 +# HTTP status code returned with fallback responses. +# 200 (default): clients treat it as success and cache the fallback image. +# 0: pass through the original upstream error status code (4xx/5xx). # PP_FALLBACK_IMAGE_HTTP_CODE=200 # Cache-Control max-age (seconds) for fallback responses. -# Falls back to PP_TTL when unset or 0. +# Falls back to PP_TTL when unset or 0. Set low (e.g. 60) to allow quick +# recovery once a missing source image is restored. # PP_FALLBACK_IMAGE_TTL= # ============================================================ # Monitoring # ============================================================ -# Address to expose Prometheus metrics endpoint. Accepts ":9464" or "0.0.0.0:9464". -# Leave empty to disable. +# Address to expose the Prometheus /metrics endpoint. +# Accepts ":9464" (all interfaces) or "127.0.0.1:9464" (loopback only). +# Leave empty to disable metrics entirely. +# Example: PP_PROMETHEUS_BIND=:9464 # PP_PROMETHEUS_BIND= -# Prefix added to all Prometheus metric names. Leave empty for no prefix. +# String prepended to all Prometheus metric names. +# Useful when multiple services share the same Prometheus instance. +# Example: PP_PROMETHEUS_NAMESPACE=pp -> pp_requests_total, pp_cache_hits_total # PP_PROMETHEUS_NAMESPACE= diff --git a/Cargo.lock b/Cargo.lock index ff81ab5..f85eb37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -32,7 +32,7 @@ checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" dependencies = [ "cfg-if", "cipher", - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -275,9 +275,9 @@ dependencies = [ [[package]] name = "aws-config" -version = "1.8.15" +version = "1.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11493b0bad143270fb8ad284a096dd529ba91924c5409adeac856cc1bf047dbc" +checksum = "50f156acdd2cf55f5aa53ee416c4ac851cf1222694506c0b1f78c85695e9ca9d" dependencies = [ "aws-credential-types", "aws-runtime", @@ -295,7 +295,7 @@ dependencies = [ "fastrand", "hex", "http 1.4.0", - "sha1", + "sha1 0.10.6", "time", "tokio", "tracing", @@ -317,9 +317,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.16.2" +version = "1.16.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a054912289d18629dc78375ba2c3726a3afe3ff71b4edba9dedfca0e3446d1fc" +checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f" dependencies = [ "aws-lc-sys", "zeroize", @@ -327,9 +327,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.39.1" +version = "0.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83a25cf98105baa966497416dbd42565ce3a8cf8dbfd59803ec9ad46f3126399" +checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7" dependencies = [ "cc", "cmake", @@ -339,9 +339,9 @@ dependencies = [ [[package]] name = "aws-runtime" -version = "1.7.2" +version = "1.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc0651c57e384202e47153c1260b84a9936e19803d747615edf199dc3b98d17" +checksum = "5dcd93c82209ac7413532388067dce79be5a8780c1786e5fae3df22e4dee2864" dependencies = [ "aws-credential-types", "aws-sigv4", @@ -367,9 +367,9 @@ dependencies = [ [[package]] name = "aws-sdk-s3" -version = "1.129.0" +version = "1.131.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d4e8410fadbc0ee453145dd77a4958227b18b05bf67c2795d0a8b8596c9aa0f" +checksum = "fe1b8c5282bf859170836045296b3cd710b7573aceb909498366bb508a41058e" dependencies = [ "aws-credential-types", "aws-runtime", @@ -388,23 +388,23 @@ dependencies = [ "bytes", "fastrand", "hex", - "hmac", + "hmac 0.13.0", "http 0.2.12", "http 1.4.0", "http-body 1.0.1", "lru", "percent-encoding", "regex-lite", - "sha2", + "sha2 0.11.0", "tracing", "url", ] [[package]] name = "aws-sdk-sso" -version = "1.97.0" +version = "1.98.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aadc669e184501caaa6beafb28c6267fc1baef0810fb58f9b205485ca3f2567" +checksum = "d69c77aafa20460c68b6b3213c84f6423b6e76dbf89accd3e1789a686ffd9489" dependencies = [ "aws-credential-types", "aws-runtime", @@ -426,9 +426,9 @@ dependencies = [ [[package]] name = "aws-sdk-ssooidc" -version = "1.99.0" +version = "1.100.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1342a7db8f358d3de0aed2007a0b54e875458e39848d54cc1d46700b2bfcb0a8" +checksum = "1c7e7b09346d5ca22a2a08267555843a6a0127fb20d8964cb6ecfb8fdb190225" dependencies = [ "aws-credential-types", "aws-runtime", @@ -450,9 +450,9 @@ dependencies = [ [[package]] name = "aws-sdk-sts" -version = "1.101.0" +version = "1.103.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab41ad64e4051ecabeea802d6a17845a91e83287e1dd249e6963ea1ba78c428a" +checksum = "c2249b81a2e73a8027c41c378463a81ec39b8510f184f2caab87de912af0f49b" dependencies = [ "aws-credential-types", "aws-runtime", @@ -475,9 +475,9 @@ dependencies = [ [[package]] name = "aws-sigv4" -version = "1.4.2" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b660013a6683ab23797778e21f1f854744fdf05f68204b4cca4c8c04b5d1f4" +checksum = "68dc0b907359b120170613b5c09ccc61304eac3998ff6274b97d93ee6490115a" dependencies = [ "aws-credential-types", "aws-smithy-eventstream", @@ -488,13 +488,13 @@ dependencies = [ "crypto-bigint 0.5.5", "form_urlencoded", "hex", - "hmac", + "hmac 0.13.0", "http 0.2.12", "http 1.4.0", "p256", "percent-encoding", "ring", - "sha2", + "sha2 0.11.0", "subtle", "time", "tracing", @@ -514,9 +514,9 @@ dependencies = [ [[package]] name = "aws-smithy-checksums" -version = "0.64.6" +version = "0.64.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6750f3dd509b0694a4377f0293ed2f9630d710b1cebe281fa8bac8f099f88bc6" +checksum = "10efbbcec1e044b81600e2fc562a391951d291152d95b482d5b7e7132299d762" dependencies = [ "aws-smithy-http", "aws-smithy-types", @@ -528,8 +528,8 @@ dependencies = [ "http-body-util", "md-5", "pin-project-lite", - "sha1", - "sha2", + "sha1 0.11.0", + "sha2 0.11.0", "tracing", ] @@ -583,11 +583,11 @@ dependencies = [ "hyper 0.14.32", "hyper 1.9.0", "hyper-rustls 0.24.2", - "hyper-rustls 0.27.7", + "hyper-rustls 0.27.9", "hyper-util", "pin-project-lite", "rustls 0.21.12", - "rustls 0.23.37", + "rustls 0.23.40", "rustls-native-certs", "rustls-pki-types", "tokio", @@ -626,9 +626,9 @@ dependencies = [ [[package]] name = "aws-smithy-runtime" -version = "1.10.3" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028999056d2d2fd58a697232f9eec4a643cf73a71cf327690a7edad1d2af2110" +checksum = "0504b1ab12debb5959e5165ee5fe97dd387e7aa7ea6a477bfd7635dfe769a4f5" dependencies = [ "aws-smithy-async", "aws-smithy-http", @@ -651,11 +651,12 @@ dependencies = [ [[package]] name = "aws-smithy-runtime-api" -version = "1.11.6" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "876ab3c9c29791ba4ba02b780a3049e21ec63dabda09268b175272c3733a79e6" +checksum = "b71a13df6ada0aafbf21a73bdfcdf9324cfa9df77d96b8446045be3cde61b42e" dependencies = [ "aws-smithy-async", + "aws-smithy-runtime-api-macros", "aws-smithy-types", "bytes", "http 0.2.12", @@ -666,6 +667,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "aws-smithy-types" version = "1.4.7" @@ -703,9 +715,9 @@ dependencies = [ [[package]] name = "aws-types" -version = "1.3.14" +version = "1.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47c8323699dd9b3c8d5b3c13051ae9cdef58fd179957c882f8374dd8725962d9" +checksum = "2f4bbcaa9304ea40902d3d5f42a0428d1bd895a2b0f6999436fb279ffddc58ac" dependencies = [ "aws-credential-types", "aws-smithy-async", @@ -717,9 +729,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "bytes", @@ -803,17 +815,17 @@ checksum = "1e4b40c7323adcfc0a41c4b88143ed58346ff65a288fc144329c5c45e05d70c6" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bitstream-io" -version = "4.9.0" +version = "4.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60d4bd9d1db2c6bdf285e223a7fa369d5ce98ec767dec949c6ca62863ce61757" +checksum = "7eff00be299a18769011411c9def0d827e8f2d7bf0c3dbf53633147a8867fd1f" dependencies = [ - "core2", + "no_std_io2", ] [[package]] @@ -825,6 +837,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -940,9 +961,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.59" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7a4d3ec6524d28a329fc53654bbadc9bdd7b0431f5d65f1a56ffb28a1ee5283" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -950,12 +971,6 @@ dependencies = [ "shlex", ] -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - [[package]] name = "cfb" version = "0.7.3" @@ -1008,15 +1023,15 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", ] [[package]] name = "clap" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" dependencies = [ "clap_builder", "clap_derive", @@ -1036,9 +1051,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.6.0" +version = "4.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" dependencies = [ "heck", "proc-macro2", @@ -1061,6 +1076,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f88a43d011fc4a6876cb7344703e297c71dda42494fee094d5f7c76bf13f746" + [[package]] name = "color_quant" version = "1.1.0" @@ -1118,6 +1139,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "core-foundation" version = "0.10.1" @@ -1134,15 +1161,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - [[package]] name = "core_maths" version = "0.1.1" @@ -1161,6 +1179,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.3.0" @@ -1172,9 +1199,9 @@ dependencies = [ [[package]] name = "crc-catalog" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc-fast" @@ -1183,7 +1210,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fd92aca2c6001b1bf5ba0ff84ee74ec8501b52bbef0cac80bf25a6c1d87a83d" dependencies = [ "crc", - "digest", + "digest 0.10.7", "rustversion", "spin", ] @@ -1269,6 +1296,24 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77727bb15fa921304124b128af125e7e3b968275d1b108b379190264f4423710" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "darling" version = "0.23.0" @@ -1347,7 +1392,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" dependencies = [ - "const-oid", + "const-oid 0.9.6", "zeroize", ] @@ -1366,11 +1411,23 @@ version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4850db49bf08e663084f7fb5c87d202ef91a3907271aff24a94eb97ff039153c" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.1", + "ctutils", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -1421,7 +1478,7 @@ dependencies = [ "base16ct", "crypto-bigint 0.4.9", "der", - "digest", + "digest 0.10.7", "ff", "generic-array", "group", @@ -1532,23 +1589,9 @@ checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fax" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05de7d48f37cd6730705cbca900770cab77a89f413d23e100ad7fad7795a0ab" -dependencies = [ - "fax_derive", -] - -[[package]] -name = "fax_derive" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" [[package]] name = "fdeflate" @@ -1793,9 +1836,9 @@ dependencies = [ [[package]] name = "gif" -version = "0.14.1" +version = "0.14.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5df2ba84018d80c213569363bdcd0c64e6933c67fe4c1d60ecf822971a3c35e" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" dependencies = [ "color_quant", "weezl", @@ -1887,6 +1930,12 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "heck" version = "0.5.0" @@ -1911,7 +1960,16 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" dependencies = [ - "digest", + "digest 0.10.7", +] + +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.2", ] [[package]] @@ -1981,6 +2039,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "0.14.32" @@ -2044,16 +2111,15 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http 1.4.0", "hyper 1.9.0", "hyper-util", - "rustls 0.23.37", + "rustls 0.23.40", "rustls-native-certs", - "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", "tower-service", @@ -2213,9 +2279,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.1" +version = "1.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" dependencies = [ "icu_normalizer", "icu_properties", @@ -2263,18 +2329,18 @@ checksum = "09e54e57b4c48b40f7aec75635392b12b3421fa26fe8b4332e63138ed278459c" [[package]] name = "imgref" -version = "1.12.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c5cedc30da3a610cac6b4ba17597bdf7152cf974e8aab3afb3d54455e371c8" +checksum = "40fac9d56ed6437b198fddba683305e8e2d651aa42647f00f5ae542e7f5c94a2" [[package]] name = "indexmap" -version = "2.13.1" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45a8a2b9cb3e0b0c1803dbb0758ffac5de2f425b23c28f518faabd9d805342ff" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -2348,27 +2414,32 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if", "combine", - "jni-sys 0.3.1", + "jni-macros", + "jni-sys", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror 2.0.18", "walkdir", - "windows-sys 0.45.0", + "windows-link", ] [[package]] -name = "jni-sys" -version = "0.3.1" +name = "jni-macros" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ - "jni-sys 0.4.1", + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn", ] [[package]] @@ -2435,9 +2506,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.94" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ "cfg-if", "futures-util", @@ -2654,9 +2725,9 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8" [[package]] name = "libc" -version = "0.2.184" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48f5d2a454e16a5ea0f4ced81bd44e4cfc7bd3a507b61887c99fd3538b28e4af" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libfuzzer-sys" @@ -2754,9 +2825,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" dependencies = [ "hashbrown 0.16.1", ] @@ -2800,12 +2871,12 @@ dependencies = [ [[package]] name = "md-5" -version = "0.10.6" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ "cfg-if", - "digest", + "digest 0.11.2", ] [[package]] @@ -2896,6 +2967,15 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "no_std_io2" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b51ed7824b6e07d354605f4abb3d9d300350701299da96642ee084f5ce631550" +dependencies = [ + "memchr", +] + [[package]] name = "nom" version = "8.0.0" @@ -3027,7 +3107,7 @@ checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" dependencies = [ "ecdsa", "elliptic-curve", - "sha2", + "sha2 0.10.9", ] [[package]] @@ -3139,9 +3219,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "png" @@ -3216,7 +3296,7 @@ dependencies = [ "dotenvy", "futures", "hex", - "hmac", + "hmac 0.12.1", "http-body-util", "httpdate", "hyper 1.9.0", @@ -3236,7 +3316,7 @@ dependencies = [ "semver", "serde", "serde_json", - "sha2", + "sha2 0.10.9", "tempfile", "thiserror 2.0.18", "tokio", @@ -3348,9 +3428,9 @@ dependencies = [ [[package]] name = "pxfm" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a041e753da8b807c9255f28de81879c78c876392ff2469cde94799b2896b9d" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" [[package]] name = "qoi" @@ -3379,7 +3459,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.37", + "rustls 0.23.40", "socket2 0.6.3", "thiserror 2.0.18", "tokio", @@ -3400,7 +3480,7 @@ dependencies = [ "rand", "ring", "rustc-hash", - "rustls 0.23.37", + "rustls 0.23.40", "rustls-pki-types", "slab", "thiserror 2.0.18", @@ -3446,9 +3526,9 @@ checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" [[package]] name = "rand" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha", "rand_core 0.9.5", @@ -3536,9 +3616,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -3600,9 +3680,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64", "bytes", @@ -3612,14 +3692,14 @@ dependencies = [ "http-body 1.0.1", "http-body-util", "hyper 1.9.0", - "hyper-rustls 0.27.7", + "hyper-rustls 0.27.9", "hyper-util", "js-sys", "log", "percent-encoding", "pin-project-lite", "quinn", - "rustls 0.23.37", + "rustls 0.23.40", "rustls-pki-types", "rustls-platform-verifier", "serde", @@ -3662,7 +3742,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" dependencies = [ "crypto-bigint 0.4.9", - "hmac", + "hmac 0.12.1", "zeroize", ] @@ -3759,14 +3839,14 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b" dependencies = [ "aws-lc-rs", "once_cell", "rustls-pki-types", - "rustls-webpki 0.103.10", + "rustls-webpki 0.103.13", "subtle", "zeroize", ] @@ -3785,9 +3865,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -3795,19 +3875,19 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation", "core-foundation-sys", "jni", "log", "once_cell", - "rustls 0.23.37", + "rustls 0.23.40", "rustls-native-certs", "rustls-platform-verifier-android", - "rustls-webpki 0.103.10", + "rustls-webpki 0.103.13", "security-framework", "security-framework-sys", "webpki-root-certs", @@ -3832,9 +3912,9 @@ dependencies = [ [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" dependencies = [ "aws-lc-rs", "ring", @@ -4031,8 +4111,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", ] [[package]] @@ -4042,8 +4133,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.2", ] [[package]] @@ -4077,7 +4179,7 @@ version = "1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" dependencies = [ - "digest", + "digest 0.10.7", "rand_core 0.6.4", ] @@ -4087,6 +4189,16 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simd_helpers" version = "0.1.0" @@ -4096,6 +4208,12 @@ dependencies = [ "quote", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "simplecss" version = "0.2.2" @@ -4420,9 +4538,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.51.1" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f66bf9585cda4b724d3e78ab34b73fb2bbaba9011b9bfdf69dc836382ea13b8c" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes", "libc", @@ -4462,7 +4580,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.37", + "rustls 0.23.40", "tokio", ] @@ -4659,9 +4777,9 @@ dependencies = [ [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "unicode-bidi" @@ -4786,9 +4904,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" +checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -4873,11 +4991,11 @@ checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -4886,14 +5004,14 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] name = "wasm-bindgen" -version = "0.2.117" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0551fc1bb415591e3372d0bc4780db7e587d84e2a7e79da121051c5c4b89d0b0" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -4904,9 +5022,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.67" +version = "0.4.70" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e" +checksum = "af934872acec734c2d80e6617bbb5ff4f12b052dd8e6332b0817bce889516084" dependencies = [ "js-sys", "wasm-bindgen", @@ -4914,9 +5032,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.117" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fbdf9a35adf44786aecd5ff89b4563a90325f9da0923236f6104e603c7e86be" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4924,9 +5042,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.117" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dca9693ef2bab6d4e6707234500350d8dad079eb508dca05530c85dc3a529ff2" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -4937,9 +5055,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.117" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39129a682a6d2d841b6c429d0c51e5cb0ed1a03829d8b3d1e69a011e62cb3d3b" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] @@ -4993,9 +5111,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.94" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd70027e39b12f0849461e08ffc50b9cd7688d942c1c8e3c7b22273236b4dd0a" +checksum = "2eadbac71025cd7b0834f20d1fe8472e8495821b4e9801eb0a60bd1f19827602" dependencies = [ "js-sys", "wasm-bindgen", @@ -5013,9 +5131,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" +checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c" dependencies = [ "rustls-pki-types", ] @@ -5094,15 +5212,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", -] - [[package]] name = "windows-sys" version = "0.52.0" @@ -5139,21 +5248,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-targets" version = "0.52.6" @@ -5187,12 +5281,6 @@ dependencies = [ "windows_x86_64_msvc 0.53.1", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -5205,12 +5293,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" @@ -5223,12 +5305,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -5253,12 +5329,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.52.6" @@ -5271,12 +5341,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" @@ -5289,12 +5353,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" @@ -5307,12 +5365,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -5327,9 +5379,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "winnow" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" [[package]] name = "wiremock" @@ -5363,6 +5415,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" diff --git a/src/common/config/loader.rs b/src/common/config/loader.rs index 5960bb0..9c8f041 100644 --- a/src/common/config/loader.rs +++ b/src/common/config/loader.rs @@ -59,6 +59,8 @@ pub struct Configuration { pub transform_disallow: HashSet, // URL aliases pub url_aliases: Option>, + // Default source URL + pub default_source_url: Option, // Best format pub best_format: BestFormatConfig, // Fallback image @@ -337,6 +339,7 @@ impl Configuration { &std::env::var("PP_TRANSFORM_DISALLOW_LIST").unwrap_or_default(), ), url_aliases: parse_url_aliases(&std::env::var("PP_URL_ALIASES").unwrap_or_default()), + default_source_url: env_var_opt("PP_DEFAULT_SOURCE_URL"), best_format: BestFormatConfig { complexity_threshold: std::env::var("PP_BEST_FORMAT_COMPLEXITY_THRESHOLD") .ok() @@ -382,6 +385,18 @@ impl Configuration { if cfg.local_enabled && cfg.local_base_dir.is_none() { panic!("LOCAL_ENABLED=true but LOCAL_BASE_DIR is not set"); } + if let Some(ref u) = cfg.default_source_url { + let valid = u.starts_with("http://") + || u.starts_with("https://") + || u.starts_with("s3:/") + || u.starts_with("local:/"); + if !valid { + panic!( + "PP_DEFAULT_SOURCE_URL must start with http://, https://, s3:/, or local:/, got {:?}", + u + ); + } + } if cfg.allowed_hosts.is_empty() { tracing::warn!("ALLOWED_HOSTS is not set - proxying requests to any host is allowed"); } @@ -480,6 +495,7 @@ impl std::fmt::Debug for Configuration { keys }), ) + .field("default_source_url", &self.default_source_url) .field("best_format", &self.best_format) .field( "fallback_image_data", @@ -882,6 +898,34 @@ mod tests { assert_eq!(cfg.ttl, 86400); } + #[test] + fn test_default_source_url_unset_is_none() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { + std::env::set_var("PP_PORT", "8080"); + std::env::set_var("PP_APP_ENV", "development"); + std::env::remove_var("PP_DEFAULT_SOURCE_URL"); + } + let cfg = super::Configuration::new(); + assert!(cfg.default_source_url.is_none()); + } + + #[test] + fn test_default_source_url_set_is_some() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { + std::env::set_var("PP_PORT", "8080"); + std::env::set_var("PP_APP_ENV", "development"); + std::env::set_var("PP_DEFAULT_SOURCE_URL", "https://cdn.example.com"); + } + let cfg = super::Configuration::new(); + unsafe { std::env::remove_var("PP_DEFAULT_SOURCE_URL") }; + assert_eq!( + cfg.default_source_url.as_deref(), + Some("https://cdn.example.com") + ); + } + #[test] fn test_fallback_image_from_env() { let _guard = ENV_LOCK.lock().unwrap(); diff --git a/src/modules/cli/args.rs b/src/modules/cli/args.rs index 5fbc499..81db650 100644 --- a/src/modules/cli/args.rs +++ b/src/modules/cli/args.rs @@ -9,31 +9,31 @@ pub struct Cli { #[arg(long, value_name = "PATH", global = true)] pub env_file: Option, - /// Server port [env: PP_PORT] + /// Server port #[arg(short, long, env = "PP_PORT", default_value = "8080")] pub port: u16, - /// Environment: development or production [env: PP_APP_ENV] + /// Environment: development or production #[arg(short = 'E', long, env = "PP_APP_ENV", default_value = "development")] pub env: String, - /// General response TTL in seconds [env: PP_TTL] + /// General response TTL in seconds #[arg(long, env = "PP_TTL", default_value_t = 86400u64)] pub ttl: u64, - /// HMAC signing key (leave empty to disable) [env: PP_HMAC_KEY] + /// HMAC signing key (leave empty to disable) #[arg(short = 'k', long, env = "PP_HMAC_KEY")] pub hmac_key: Option, - /// Comma-separated allowed upstream hosts (empty = allow all) [env: PP_ALLOWED_HOSTS] + /// Comma-separated allowed upstream hosts (empty = allow all) #[arg(short = 'a', long, env = "PP_ALLOWED_HOSTS", default_value = "")] pub allowed_hosts: String, - /// Upstream fetch timeout in seconds [env: PP_FETCH_TIMEOUT_SECS] + /// Upstream fetch timeout in seconds #[arg(short = 't', long, env = "PP_FETCH_TIMEOUT_SECS", default_value = "10")] pub fetch_timeout_secs: u64, - /// Maximum source image size in bytes [env: PP_MAX_SOURCE_BYTES] + /// Maximum source image size in bytes #[arg( short = 's', long, @@ -42,15 +42,15 @@ pub struct Cli { )] pub max_source_bytes: u64, - /// L1 in-memory cache size in MB [env: PP_CACHE_MEMORY_MAX_MB] + /// L1 in-memory cache size in MB #[arg(long, env = "PP_CACHE_MEMORY_MAX_MB", default_value = "256")] pub cache_memory_max_mb: u64, - /// L1 in-memory cache TTL in seconds [env: PP_CACHE_MEMORY_TTL_SECS] + /// L1 in-memory cache TTL in seconds #[arg(long, env = "PP_CACHE_MEMORY_TTL_SECS", default_value = "3600")] pub cache_memory_ttl_secs: u64, - /// L2 disk cache directory [env: PP_CACHE_DIR] + /// L2 disk cache directory #[arg( short = 'D', long, @@ -59,55 +59,59 @@ pub struct Cli { )] pub cache_dir: String, - /// L2 disk cache TTL in seconds [env: PP_CACHE_DISK_TTL_SECS] + /// L2 disk cache TTL in seconds #[arg(long, env = "PP_CACHE_DISK_TTL_SECS", default_value = "86400")] pub cache_disk_ttl_secs: u64, - /// L2 disk cache max size in MB (empty = unlimited) [env: PP_CACHE_DISK_MAX_MB] + /// L2 disk cache max size in MB (empty = unlimited) #[arg(long, env = "PP_CACHE_DISK_MAX_MB", default_value = "")] pub cache_disk_max_mb: String, - /// Cache cleanup interval in seconds [env: PP_CACHE_CLEANUP_INTERVAL_SECS] + /// Cache cleanup interval in seconds #[arg(long, env = "PP_CACHE_CLEANUP_INTERVAL_SECS", default_value = "600")] pub cache_cleanup_interval_secs: u64, - /// Path to the ffmpeg binary [env: PP_FFMPEG_PATH] + /// Path to the ffmpeg binary #[arg(long, env = "PP_FFMPEG_PATH", default_value = "ffmpeg")] pub ffmpeg_path: String, - /// Path to the ffprobe binary (defaults to ffprobe in same dir as ffmpeg) [env: PP_FFPROBE_PATH] + /// Path to the ffprobe binary (defaults to ffprobe in same dir as ffmpeg) #[arg(long, env = "PP_FFPROBE_PATH", default_value = "")] pub ffprobe_path: String, - /// Comma-separated allowed CORS origins; * = allow all [env: PP_CORS_ALLOW_ORIGIN] + /// Comma-separated allowed CORS origins; * = allow all #[arg(long, env = "PP_CORS_ALLOW_ORIGIN", default_value = "*")] pub cors_allow_origin: String, - /// CORS max-age in seconds [env: PP_CORS_MAX_AGE_SECS] + /// CORS max-age in seconds #[arg(long, env = "PP_CORS_MAX_AGE_SECS", default_value = "600")] pub cors_max_age_secs: u64, - /// Comma-separated input formats to block (jpeg,png,gif,webp,avif,jxl,bmp,tiff,pdf,psd,video) [env: PP_INPUT_DISALLOW_LIST] + /// Comma-separated input formats to block (jpeg,png,gif,webp,avif,jxl,bmp,tiff,pdf,psd,video) #[arg(long, env = "PP_INPUT_DISALLOW_LIST", default_value = "")] pub input_disallow_list: String, - /// Comma-separated output formats to block (jpeg,png,gif,webp,avif,jxl,bmp,tiff,ico) [env: PP_OUTPUT_DISALLOW_LIST] + /// Comma-separated output formats to block (jpeg,png,gif,webp,avif,jxl,bmp,tiff,ico) #[arg(long, env = "PP_OUTPUT_DISALLOW_LIST", default_value = "")] pub output_disallow_list: String, - /// Comma-separated transforms to block (resize,rotate,flip,grayscale,brightness,contrast,blur,watermark,gif_anim) [env: PP_TRANSFORM_DISALLOW_LIST] + /// Comma-separated transforms to block (resize,rotate,flip,grayscale,brightness,contrast,blur,watermark,gif_anim) #[arg(long, env = "PP_TRANSFORM_DISALLOW_LIST", default_value = "")] pub transform_disallow_list: String, - /// URL alias definitions: name=https://base.url,name2=https://other.url; enables name:/path scheme in requests [env: PP_URL_ALIASES] + /// URL alias definitions: name=https://base.url,name2=https://other.url; enables name:/path scheme in requests #[arg(long, env = "PP_URL_ALIASES", default_value = "")] pub url_aliases: String, - /// Max in-flight requests before returning 503 [env: PP_MAX_CONCURRENT_REQUESTS] + /// Base URL prepended to relative request paths (http://, https://, s3:/, local:/) + #[arg(long, env = "PP_DEFAULT_SOURCE_URL")] + pub default_source_url: Option, + + /// Max in-flight requests before returning 503 #[arg(long, env = "PP_MAX_CONCURRENT_REQUESTS", default_value_t = 256)] pub max_concurrent_requests: u32, - /// Log level filter (e.g. previewproxy=info,tower_http=info) [env: RUST_LOG] + /// Log level filter (e.g. previewproxy=info,tower_http=info) #[arg( long, env = "RUST_LOG", @@ -115,43 +119,43 @@ pub struct Cli { )] pub rust_log: String, - /// Hex-encoded AES key for encrypting/decrypting source URLs (leave empty to disable) [env: PP_SOURCE_URL_ENCRYPTION_KEY] + /// Hex-encoded AES key for encrypting/decrypting source URLs (leave empty to disable) #[arg(long, env = "PP_SOURCE_URL_ENCRYPTION_KEY")] pub source_url_encryption_key: Option, - /// Enable S3 as an image source [env: PP_S3_ENABLED] + /// Enable S3 as an image source #[arg(long, env = "PP_S3_ENABLED", default_value_t = false)] pub s3_enabled: bool, - /// S3 bucket name [env: PP_S3_BUCKET] + /// S3 bucket name #[arg(long, env = "PP_S3_BUCKET", default_value = "")] pub s3_bucket: String, - /// S3 region [env: PP_S3_REGION] + /// S3 region #[arg(long, env = "PP_S3_REGION", default_value = "us-east-1")] pub s3_region: String, - /// S3 access key ID [env: PP_S3_ACCESS_KEY_ID] + /// S3 access key ID #[arg(long, env = "PP_S3_ACCESS_KEY_ID", default_value = "")] pub s3_access_key_id: String, - /// S3 secret access key [env: PP_S3_SECRET_ACCESS_KEY] + /// S3 secret access key #[arg(long, env = "PP_S3_SECRET_ACCESS_KEY", default_value = "")] pub s3_secret_access_key: String, - /// S3 custom endpoint URL (leave empty for AWS) [env: PP_S3_ENDPOINT] + /// S3 custom endpoint URL (leave empty for AWS) #[arg(long, env = "PP_S3_ENDPOINT", default_value = "")] pub s3_endpoint: String, - /// Enable serving images from the local filesystem [env: PP_LOCAL_ENABLED] + /// Enable serving images from the local filesystem #[arg(long, env = "PP_LOCAL_ENABLED", default_value_t = false)] pub local_enabled: bool, - /// Absolute path to root directory for local image files [env: PP_LOCAL_BASE_DIR] + /// Absolute path to root directory for local image files #[arg(long, env = "PP_LOCAL_BASE_DIR", default_value = "")] pub local_base_dir: String, - /// Edge density threshold (0-100) for best-format complexity classification [env: PP_BEST_FORMAT_COMPLEXITY_THRESHOLD] + /// Edge density threshold (0-100) for best-format complexity classification #[arg( long, env = "PP_BEST_FORMAT_COMPLEXITY_THRESHOLD", @@ -159,19 +163,19 @@ pub struct Cli { )] pub best_format_complexity_threshold: f64, - /// Max resolution in megapixels before skipping multi-format trial (leave empty to always trial) [env: PP_BEST_FORMAT_MAX_RESOLUTION] + /// Max resolution in megapixels before skipping multi-format trial (leave empty to always trial) #[arg(long, env = "PP_BEST_FORMAT_MAX_RESOLUTION", default_value = "")] pub best_format_max_resolution: String, - /// Apply best-format selection for all requests that don't specify a format [env: PP_BEST_FORMAT_BY_DEFAULT] + /// Apply best-format selection for all requests that don't specify a format #[arg(long, env = "PP_BEST_FORMAT_BY_DEFAULT", default_value_t = false)] pub best_format_by_default: bool, - /// Skip re-encoding if selected best format matches source format and no transforms applied [env: PP_BEST_FORMAT_ALLOW_SKIPS] + /// Skip re-encoding if selected best format matches source format and no transforms applied #[arg(long, env = "PP_BEST_FORMAT_ALLOW_SKIPS", default_value_t = false)] pub best_format_allow_skips: bool, - /// Comma-separated formats to trial for best-format selection [env: PP_BEST_FORMAT_PREFERRED_FORMATS] + /// Comma-separated formats to trial for best-format selection #[arg( long, env = "PP_BEST_FORMAT_PREFERRED_FORMATS", @@ -179,31 +183,31 @@ pub struct Cli { )] pub best_format_preferred_formats: String, - /// Address to expose Prometheus metrics (e.g. :9464); leave empty to disable [env: PP_PROMETHEUS_BIND] + /// Address to expose Prometheus metrics (e.g. :9464); leave empty to disable #[arg(long, env = "PP_PROMETHEUS_BIND", default_value = "")] pub prometheus_bind: String, - /// Prefix for all Prometheus metric names [env: PP_PROMETHEUS_NAMESPACE] + /// Prefix for all Prometheus metric names #[arg(long, env = "PP_PROMETHEUS_NAMESPACE", default_value = "")] pub prometheus_namespace: String, - /// Base64-encoded fallback image data [env: PP_FALLBACK_IMAGE_DATA] + /// Base64-encoded fallback image data #[arg(long, env = "PP_FALLBACK_IMAGE_DATA")] pub fallback_image_data: Option, - /// Path to local fallback image file [env: PP_FALLBACK_IMAGE_PATH] + /// Path to local fallback image file #[arg(long, env = "PP_FALLBACK_IMAGE_PATH", default_value = "")] pub fallback_image_path: String, - /// URL of fallback image [env: PP_FALLBACK_IMAGE_URL] + /// URL of fallback image #[arg(long, env = "PP_FALLBACK_IMAGE_URL", default_value = "")] pub fallback_image_url: String, - /// HTTP status code for fallback responses; 0 = use original error code [env: PP_FALLBACK_IMAGE_HTTP_CODE] + /// HTTP status code for fallback responses; 0 = use original error code #[arg(long, env = "PP_FALLBACK_IMAGE_HTTP_CODE", default_value_t = 200u16)] pub fallback_image_http_code: u16, - /// TTL in seconds for fallback image responses; 0 = use PP_TTL [env: PP_FALLBACK_IMAGE_TTL] + /// TTL in seconds for fallback image responses; 0 = use PP_TTL #[arg(long, env = "PP_FALLBACK_IMAGE_TTL", default_value_t = 0u64)] pub fallback_image_ttl: u64, @@ -255,6 +259,10 @@ impl Cli { "PP_SOURCE_URL_ENCRYPTION_KEY", self.source_url_encryption_key.as_deref().unwrap_or(""), ); + std::env::set_var( + "PP_DEFAULT_SOURCE_URL", + self.default_source_url.as_deref().unwrap_or(""), + ); std::env::set_var("PP_S3_ENABLED", self.s3_enabled.to_string()); std::env::set_var("PP_S3_BUCKET", &self.s3_bucket); std::env::set_var("PP_S3_REGION", &self.s3_region); diff --git a/src/modules/proxy/controller.rs b/src/modules/proxy/controller.rs index 2de08df..46e8900 100644 --- a/src/modules/proxy/controller.rs +++ b/src/modules/proxy/controller.rs @@ -11,7 +11,6 @@ use crate::modules::proxy::{ }, service::ProxyService, }; -use crate::modules::security::encryption; use axum::{ Router, extract::{Path, Query, State}, @@ -24,13 +23,6 @@ use std::collections::HashMap; use std::time::Instant; use tokio::sync::OwnedSemaphorePermit; -fn decrypt_url(key: Option<&Vec>, blob: &str) -> Result { - let key = key.ok_or_else(|| { - ProxyError::InvalidParams("source URL encryption key not configured".to_string()) - })?; - encryption::decrypt(key, blob).map_err(|e| ProxyError::InvalidParams(e.to_string())) -} - /// Registers the two proxy entry points: /// - `GET /proxy?url=&` - query-string style /// - `GET //` - path style (params encoded in path prefix) @@ -143,15 +135,12 @@ async fn handle_query_inner( .get("url") .cloned() .ok_or_else(|| ProxyError::InvalidParams("missing `url` query param".to_string()))?; - // Presence of `enc` key (any value) signals the URL is encrypted. - let url = if query.contains_key("enc") { - decrypt_url(state.cfg.source_url_encryption_key.as_ref(), &raw_url)? - } else { - raw_url - }; + let encrypted = query.contains_key("enc"); let params = from_query(&query)?; let service = ProxyService::new(&state); - let result = service.process(params, url, permit, queued_at).await; + let result = service + .process(params, raw_url, encrypted, permit, queued_at) + .await; match result { Ok(r) => Ok(build_response(r, &state.cfg)), Err(ref e) if is_upstream_error(e) => { @@ -173,9 +162,9 @@ async fn handle_path_inner( queued_at: Instant, ) -> Result { let (mut params, raw_url) = TransformParams::from_path(&path)?; - let url = if raw_url.starts_with("enc/") { - let blob = &raw_url["enc/".len()..]; - decrypt_url(state.cfg.source_url_encryption_key.as_ref(), blob)? + let encrypted = raw_url.starts_with("enc/"); + let raw_url = if encrypted { + raw_url["enc/".len()..].to_string() } else { raw_url }; @@ -184,7 +173,9 @@ async fn handle_path_inner( params.merge_from(query_params); } let svc = ProxyService::new(&state); - let result = svc.process(params, url, permit, queued_at).await; + let result = svc + .process(params, raw_url, encrypted, permit, queued_at) + .await; match result { Ok(r) => Ok(build_response(r, &state.cfg)), Err(ref e) if is_upstream_error(e) => { @@ -347,6 +338,7 @@ mod concurrency_tests { fallback_image_http_code: 200, fallback_image_ttl: None, ttl: 86400, + default_source_url: None, }); let http = Arc::new( HttpFetcher::new(10, 1_000_000, Arc::new(Allowlist::new(vec![]))) @@ -738,6 +730,80 @@ mod concurrency_tests { ); } + #[tokio::test] + async fn test_query_enc_flag_passed_as_encrypted_true() { + // When ?enc= is present and no key is configured, service returns 400 (proving encrypted=true was passed) + let state = make_state_with_enc_key(256, None); + let app = crate::modules::router(state); + let req = axum::http::Request::builder() + .uri("/proxy?url=anyblob&enc=1") + .body(axum::body::Body::empty()) + .unwrap(); + let resp = app.oneshot(req).await.unwrap(); + assert_eq!(resp.status(), axum::http::StatusCode::BAD_REQUEST); + } + + #[tokio::test] + async fn test_path_enc_prefix_passed_as_encrypted_true() { + let state = make_state_with_enc_key(256, None); + let app = crate::modules::router(state); + let req = axum::http::Request::builder() + .uri("/enc/someblob") + .body(axum::body::Body::empty()) + .unwrap(); + let resp = app.oneshot(req).await.unwrap(); + assert_eq!(resp.status(), axum::http::StatusCode::BAD_REQUEST); + } + + fn make_state_with_default_source_url(server_uri: String) -> AppState { + let mut cfg = (*make_state(256).cfg).clone(); + cfg.default_source_url = Some(server_uri); + AppState { + cfg: std::sync::Arc::new(cfg), + ..make_state(256) + } + } + + #[tokio::test] + async fn test_relative_url_resolved_via_default_source_url() { + use http_body_util::BodyExt; + use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method}; + + let server = MockServer::start().await; + Mock::given(method("GET")) + .respond_with( + ResponseTemplate::new(200) + .set_body_bytes(vec![1u8; 10]) + .insert_header("content-type", "image/png"), + ) + .mount(&server) + .await; + + let state = make_state_with_default_source_url(server.uri()); + let app = crate::modules::router(state); + + let req = axum::http::Request::builder() + .uri("/proxy?url=%2Fimg.png") + .body(axum::body::Body::empty()) + .unwrap(); + let resp = app.oneshot(req).await.unwrap(); + assert_eq!(resp.status(), axum::http::StatusCode::OK); + let _ = resp.into_body().collect().await.unwrap(); + } + + #[tokio::test] + async fn test_relative_url_without_default_source_url_returns_400() { + let state = make_state(256); + let app = crate::modules::router(state); + + let req = axum::http::Request::builder() + .uri("/proxy?url=%2Fimg.png") + .body(axum::body::Body::empty()) + .unwrap(); + let resp = app.oneshot(req).await.unwrap(); + assert_eq!(resp.status(), axum::http::StatusCode::BAD_REQUEST); + } + #[tokio::test] async fn test_fallback_ttl_falls_back_to_pp_ttl() { use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method}; @@ -780,4 +846,44 @@ mod concurrency_tests { Some("public, max-age=1234") ); } + + #[tokio::test] + async fn test_path_style_relative_url_resolved_via_default_source_url() { + use http_body_util::BodyExt; + use wiremock::{Mock, MockServer, ResponseTemplate, matchers::method}; + + let server = MockServer::start().await; + Mock::given(method("GET")) + .respond_with( + ResponseTemplate::new(200) + .set_body_bytes(vec![1u8; 10]) + .insert_header("content-type", "image/png"), + ) + .mount(&server) + .await; + + let state = make_state_with_default_source_url(server.uri()); + let app = crate::modules::router(state); + + let req = axum::http::Request::builder() + .uri("/build/logo.BnUd846k_Z10Q0xM.webp") + .body(axum::body::Body::empty()) + .unwrap(); + let resp = app.oneshot(req).await.unwrap(); + assert_eq!(resp.status(), axum::http::StatusCode::OK); + let _ = resp.into_body().collect().await.unwrap(); + } + + #[tokio::test] + async fn test_path_style_relative_url_without_default_source_url_returns_400() { + let state = make_state(256); + let app = crate::modules::router(state); + + let req = axum::http::Request::builder() + .uri("/build/logo.webp") + .body(axum::body::Body::empty()) + .unwrap(); + let resp = app.oneshot(req).await.unwrap(); + assert_eq!(resp.status(), axum::http::StatusCode::BAD_REQUEST); + } } diff --git a/src/modules/proxy/dto/params.rs b/src/modules/proxy/dto/params.rs index 0f0dca9..cc1e8bf 100644 --- a/src/modules/proxy/dto/params.rs +++ b/src/modules/proxy/dto/params.rs @@ -125,20 +125,22 @@ impl TransformParams { let (opts_str, url) = if let Some(pos) = split_pos { (&path[..pos], &path[pos + 1..]) - } else if path.starts_with("https://") - || path.starts_with("http://") - || path.starts_with("s3:/") - || path.starts_with("local:/") - || path.starts_with("local:%2F") - || path.starts_with("local:%2f") - || path.starts_with("enc/") - || (path.contains(":/") && !path.contains("://")) - { - ("", path) } else { - return Err(ProxyError::InvalidParams( - "No image URL found in path".to_string(), - )); + // No scheme delimiter found. If the segment before the first '/' parses as + // valid transform options, the caller wrote e.g. `300x200/my/path.jpg` but + // forgot to include an absolute URL — that is ambiguous, so we error. + // Otherwise treat the whole path as a relative URL and let the service + // resolve it against PP_DEFAULT_SOURCE_URL. + if let Some(slash) = path.find('/') { + let prefix = &path[..slash]; + if !prefix.is_empty() && parse_options(prefix).is_ok() { + return Err(ProxyError::InvalidParams( + "transform options require an absolute source URL (e.g. /300x200/https://...)" + .to_string(), + )); + } + } + ("", path) }; let url = urlencoding::decode(url) @@ -1874,4 +1876,38 @@ mod tests { assert_eq!(p.wmt_size, Some(18)); assert_eq!(p.wmt_font, Some("sans".to_string())); } + + #[test] + fn test_relative_path_no_scheme_parsed_as_url() { + let (params, url) = TransformParams::from_path("build/logo.BnUd846k_Z10Q0xM.webp").unwrap(); + assert!(params.w.is_none()); + assert!(params.format.is_none()); + assert_eq!(url, "build/logo.BnUd846k_Z10Q0xM.webp"); + } + + #[test] + fn test_relative_path_with_leading_slash_parsed_as_url() { + let (params, url) = TransformParams::from_path("/assets/img.png").unwrap(); + assert!(params.w.is_none()); + assert_eq!(url, "/assets/img.png"); + } + + #[test] + fn test_transforms_before_relative_path_errors() { + // `300x200` is valid transform syntax but there is no absolute URL after it. + let result = TransformParams::from_path("300x200/build/logo.webp"); + assert!( + matches!(result, Err(ProxyError::InvalidParams(ref m)) if m.contains("absolute source URL")), + "got: {result:?}" + ); + } + + #[test] + fn test_format_transform_before_relative_path_errors() { + let result = TransformParams::from_path("webp/build/logo.webp"); + assert!( + matches!(result, Err(ProxyError::InvalidParams(ref m)) if m.contains("absolute source URL")), + "got: {result:?}" + ); + } } diff --git a/src/modules/proxy/fallback.rs b/src/modules/proxy/fallback.rs index 3943533..2d898d3 100644 --- a/src/modules/proxy/fallback.rs +++ b/src/modules/proxy/fallback.rs @@ -89,6 +89,7 @@ mod tests { listen_address: SocketAddr::from((Ipv4Addr::UNSPECIFIED, 8080)), app_port: 8080, hmac_key: None, + ttl: 86400, source_url_encryption_key: None, allowed_hosts: vec![], fetch_timeout_secs: 10, @@ -118,6 +119,7 @@ mod tests { output_disallow: HashSet::new(), transform_disallow: HashSet::new(), url_aliases: None, + default_source_url: None, best_format: Default::default(), prometheus_bind: None, prometheus_namespace: String::new(), @@ -126,7 +128,6 @@ mod tests { fallback_image_url: None, fallback_image_http_code: 200, fallback_image_ttl: None, - ttl: 86400, } } diff --git a/src/modules/proxy/service.rs b/src/modules/proxy/service.rs index 4f98870..b785efb 100644 --- a/src/modules/proxy/service.rs +++ b/src/modules/proxy/service.rs @@ -22,6 +22,8 @@ pub struct ProxyService { cache: Arc, allowlist: Allowlist, hmac_key: Option, + source_url_encryption_key: Option>, + default_source_url: Option, ffmpeg_path: String, ffprobe_path: String, max_source_bytes: u64, @@ -41,6 +43,8 @@ impl ProxyService { cache: state.cache.clone(), allowlist, hmac_key: state.cfg.hmac_key.clone(), + source_url_encryption_key: state.cfg.source_url_encryption_key.clone(), + default_source_url: state.cfg.default_source_url.clone(), ffmpeg_path: state.cfg.ffmpeg_path.clone(), ffprobe_path: state.cfg.ffprobe_path.clone(), max_source_bytes: state.cfg.max_source_bytes, @@ -52,6 +56,33 @@ impl ProxyService { } } + fn resolve_url(&self, raw: &str, encrypted: bool) -> Result { + use crate::modules::security::encryption; + + let url = if encrypted { + let key = self.source_url_encryption_key.as_ref().ok_or_else(|| { + ProxyError::InvalidParams("source URL encryption key not configured".to_string()) + })?; + encryption::decrypt(key, raw).map_err(|e| ProxyError::InvalidParams(e.to_string()))? + } else { + raw.to_string() + }; + + let has_scheme = url.contains("://") || url.contains(":/"); + if has_scheme { + return Ok(url); + } + + let base = self + .default_source_url + .as_deref() + .ok_or_else(|| ProxyError::InvalidParams("Invalid source URL".to_string()))?; + + let base = base.trim_end_matches('/'); + let path = url.trim_start_matches('/'); + Ok(format!("{base}/{path}")) + } + /// Full request pipeline: /// /// 1. Allowlist check on image URL and watermark URL hosts @@ -69,7 +100,8 @@ impl ProxyService { pub async fn process( &self, params: TransformParams, - image_url: String, + raw_url: String, + encrypted: bool, permit: OwnedSemaphorePermit, queued_at: Instant, ) -> Result { @@ -102,6 +134,8 @@ impl ProxyService { .with_label_values(&["queue"]) .observe(queued_at.elapsed().as_secs_f64()); + let image_url = self.resolve_url(&raw_url, encrypted)?; + // 1. Allowlist check for image URL host (HTTP/HTTPS only) if image_url.starts_with("http://") || image_url.starts_with("https://") { let image_host = Url::parse(&image_url) @@ -645,6 +679,7 @@ mod tests { fallback_image_http_code: 200, fallback_image_ttl: None, ttl: 86400, + default_source_url: None, }) } @@ -680,6 +715,8 @@ mod tests { cache, allowlist: Allowlist::new(allowed_hosts), hmac_key: None, + source_url_encryption_key: None, + default_source_url: None, ffmpeg_path: "ffmpeg".to_string(), ffprobe_path: "ffprobe".to_string(), max_source_bytes: 1_000_000, @@ -700,6 +737,7 @@ mod tests { .process( params, "s3:/some/key.jpg".to_string(), + false, Arc::new(tokio::sync::Semaphore::new(1)) .try_acquire_owned() .unwrap(), @@ -728,6 +766,7 @@ mod tests { .process( params, "s3:/images/photo.jpg".to_string(), + false, Arc::new(tokio::sync::Semaphore::new(1)) .try_acquire_owned() .unwrap(), @@ -770,6 +809,8 @@ mod tests { cache, allowlist: Allowlist::new(vec![]), hmac_key: None, + source_url_encryption_key: None, + default_source_url: None, ffmpeg_path: "ffmpeg".to_string(), ffprobe_path: "ffprobe".to_string(), max_source_bytes: 1_000_000, @@ -785,6 +826,7 @@ mod tests { .process( params, "s3:/v.mp4".to_string(), + false, Arc::new(tokio::sync::Semaphore::new(1)) .try_acquire_owned() .unwrap(), @@ -830,6 +872,8 @@ mod tests { cache, allowlist: Allowlist::new(vec![]), hmac_key: None, + source_url_encryption_key: None, + default_source_url: None, ffmpeg_path: "ffmpeg".to_string(), ffprobe_path: "ffprobe".to_string(), max_source_bytes: 1_000_000, @@ -844,6 +888,7 @@ mod tests { .process( params, "s3:/v.mp4".to_string(), + false, Arc::new(tokio::sync::Semaphore::new(1)) .try_acquire_owned() .unwrap(), @@ -913,6 +958,7 @@ mod streaming_tests { fallback_image_http_code: 200, fallback_image_ttl: None, ttl: 86400, + default_source_url: None, }); let http = Arc::new( HttpFetcher::new(10, max_bytes, Arc::new(Allowlist::new(vec![]))) @@ -925,6 +971,8 @@ mod streaming_tests { cache: cache.clone(), allowlist: Allowlist::new(vec![]), hmac_key: None, + source_url_encryption_key: None, + default_source_url: None, ffmpeg_path: "ffmpeg".to_string(), ffprobe_path: "ffprobe".to_string(), max_source_bytes: max_bytes, @@ -957,6 +1005,7 @@ mod streaming_tests { .process( TransformParams::default(), server.uri(), + false, permit(), std::time::Instant::now(), ) @@ -981,6 +1030,7 @@ mod streaming_tests { .process( TransformParams::default(), server.uri(), + false, permit(), std::time::Instant::now(), ) @@ -1007,6 +1057,7 @@ mod streaming_tests { .process( TransformParams::default(), server.uri(), + false, permit(), std::time::Instant::now(), ) @@ -1034,6 +1085,7 @@ mod streaming_tests { .process( TransformParams::default(), server.uri(), + false, permit(), std::time::Instant::now(), ) @@ -1058,6 +1110,7 @@ mod streaming_tests { .process( TransformParams::default(), url.clone(), + false, permit(), std::time::Instant::now(), ) @@ -1083,6 +1136,7 @@ mod streaming_tests { .process( TransformParams::default(), "s3:/some/key.jpg".to_string(), + false, permit(), std::time::Instant::now(), ) @@ -1107,6 +1161,7 @@ mod streaming_tests { .process( TransformParams::default(), url.clone(), + false, permit(), std::time::Instant::now(), ) @@ -1147,6 +1202,7 @@ mod streaming_tests { .process( TransformParams::default(), format!("{}/img.png", server.uri()), + false, permit(), std::time::Instant::now(), ) @@ -1175,6 +1231,7 @@ mod streaming_tests { .process( TransformParams::default(), url.clone(), + false, permit(), std::time::Instant::now(), ) @@ -1194,3 +1251,179 @@ mod streaming_tests { ); } } + +#[cfg(test)] +mod url_resolution_tests { + use super::*; + + fn svc_with_defaults( + default_source_url: Option, + enc_key: Option>, + ) -> ProxyService { + use crate::common::config::Configuration; + use crate::modules::cache::manager::CacheManager; + use crate::modules::proxy::sources::http::HttpFetcher; + use crate::modules::security::allowlist::Allowlist; + use std::net::{Ipv4Addr, SocketAddr}; + + let cfg = Arc::new(Configuration { + env: crate::common::config::Environment::Development, + listen_address: SocketAddr::from((Ipv4Addr::UNSPECIFIED, 8080)), + app_port: 8080, + hmac_key: None, + source_url_encryption_key: enc_key.clone(), + allowed_hosts: vec![], + fetch_timeout_secs: 10, + fetch_retry_count: 0, + fetch_retry_delay_ms: 0, + max_source_bytes: 1_000_000, + cache_memory_max_mb: 16, + cache_memory_ttl_secs: 60, + cache_dir: "/tmp/previewproxy-url-test".to_string(), + cache_disk_ttl_secs: 60, + cache_disk_max_mb: None, + cache_cleanup_interval_secs: 600, + s3_enabled: false, + s3_bucket: None, + s3_region: "us-east-1".to_string(), + s3_access_key_id: None, + s3_secret_access_key: None, + s3_endpoint: None, + local_enabled: false, + local_base_dir: None, + ffmpeg_path: "ffmpeg".to_string(), + ffprobe_path: "ffprobe".to_string(), + cors_allow_origin: vec!["*".to_string()], + cors_max_age_secs: 600, + max_concurrent_requests: 256, + input_disallow: std::collections::HashSet::new(), + output_disallow: std::collections::HashSet::new(), + transform_disallow: std::collections::HashSet::new(), + url_aliases: None, + best_format: Default::default(), + prometheus_bind: None, + prometheus_namespace: String::new(), + fallback_image_data: None, + fallback_image_path: None, + fallback_image_url: None, + fallback_image_http_code: 200, + fallback_image_ttl: None, + ttl: 86400, + default_source_url: default_source_url.clone(), + }); + let http = Arc::new( + HttpFetcher::new(10, 1_000_000, Arc::new(Allowlist::new(vec![]))) + .with_private_ip_check(false), + ); + let cache = CacheManager::new(&cfg, crate::modules::metrics::Metrics::new("")); + ProxyService { + fetcher: http.clone(), + http_fetcher: http, + cache, + allowlist: Allowlist::new(vec![]), + hmac_key: None, + source_url_encryption_key: enc_key, + default_source_url, + ffmpeg_path: "ffmpeg".to_string(), + ffprobe_path: "ffprobe".to_string(), + max_source_bytes: 1_000_000, + input_disallow: std::collections::HashSet::new(), + output_disallow: std::collections::HashSet::new(), + transform_disallow: std::collections::HashSet::new(), + best_format: crate::common::config::BestFormatConfig::default(), + metrics: crate::modules::metrics::Metrics::new(""), + } + } + + #[test] + fn test_resolve_url_absolute_http_unchanged() { + let svc = svc_with_defaults(None, None); + let result = svc + .resolve_url("https://cdn.example.com/img.jpg", false) + .unwrap(); + assert_eq!(result, "https://cdn.example.com/img.jpg"); + } + + #[test] + fn test_resolve_url_s3_unchanged() { + let svc = svc_with_defaults(None, None); + let result = svc.resolve_url("s3:/bucket/key.jpg", false).unwrap(); + assert_eq!(result, "s3:/bucket/key.jpg"); + } + + #[test] + fn test_resolve_url_local_unchanged() { + let svc = svc_with_defaults(None, None); + let result = svc.resolve_url("local:/images/img.jpg", false).unwrap(); + assert_eq!(result, "local:/images/img.jpg"); + } + + #[test] + fn test_resolve_url_alias_unchanged() { + let svc = svc_with_defaults(None, None); + let result = svc.resolve_url("mycdn:/img.jpg", false).unwrap(); + assert_eq!(result, "mycdn:/img.jpg"); + } + + #[test] + fn test_resolve_url_relative_prepends_base() { + let svc = svc_with_defaults(Some("https://cdn.example.com".to_string()), None); + let result = svc.resolve_url("/photo.jpg", false).unwrap(); + assert_eq!(result, "https://cdn.example.com/photo.jpg"); + } + + #[test] + fn test_resolve_url_relative_base_trailing_slash() { + let svc = svc_with_defaults(Some("https://cdn.example.com/".to_string()), None); + let result = svc.resolve_url("/photo.jpg", false).unwrap(); + assert_eq!(result, "https://cdn.example.com/photo.jpg"); + } + + #[test] + fn test_resolve_url_relative_no_leading_slash() { + let svc = svc_with_defaults(Some("https://cdn.example.com".to_string()), None); + let result = svc.resolve_url("photo.jpg", false).unwrap(); + assert_eq!(result, "https://cdn.example.com/photo.jpg"); + } + + #[test] + fn test_resolve_url_relative_no_default_errors() { + let svc = svc_with_defaults(None, None); + let result = svc.resolve_url("/photo.jpg", false); + assert!( + matches!(result, Err(ProxyError::InvalidParams(ref m)) if m.contains("Invalid source URL")), + "got: {:?}", + result + ); + } + + #[test] + fn test_resolve_url_encrypted_decrypts() { + let key = b"01234567890123456789012345678901".to_vec(); + let plaintext = "https://cdn.example.com/img.jpg"; + let blob = crate::modules::security::encryption::encrypt(&key, plaintext).unwrap(); + let svc = svc_with_defaults(None, Some(key)); + let result = svc.resolve_url(&blob, true).unwrap(); + assert_eq!(result, plaintext); + } + + #[test] + fn test_resolve_url_encrypted_no_key_errors() { + let svc = svc_with_defaults(None, None); + let result = svc.resolve_url("someblob", true); + assert!( + matches!(result, Err(ProxyError::InvalidParams(ref m)) if m.contains("encryption key not configured")), + "got: {:?}", + result + ); + } + + #[test] + fn test_resolve_url_encrypted_then_relative_prepends_base() { + let key = b"01234567890123456789012345678901".to_vec(); + let blob = crate::modules::security::encryption::encrypt(&key, "/photo.jpg").unwrap(); + let svc = svc_with_defaults(Some("https://cdn.example.com".to_string()), Some(key)); + let result = svc.resolve_url(&blob, true).unwrap(); + assert_eq!(result, "https://cdn.example.com/photo.jpg"); + } +}