Skip to content

Repository files navigation

PreviewProxy

Latest ReleaseCIRustAxumLicense: Apache 2.0GitHub Container RegistryGitHub starsGitHub issues

A fast, self-hosted image proxy written in Rust. Fetch images from HTTP URLs, S3 buckets, or local storage - transform them on-the-fly and serve them with multi-tier caching.

Features

  • On-the-fly transforms - resize, crop, rotate, flip, grayscale, brightness/contrast, blur, watermark, format conversion (JPEG, PNG, WebP, AVIF, JXL)
  • Multiple sources - remote HTTP URLs, S3-compatible buckets, and local filesystem
  • Two request styles - path-style (/300x200,webp/https://example.com/img.jpg) and query-style (/proxy?url=...&w=300&h=200)
  • Animated GIF pipeline - output all frames of an animated GIF, optionally applying transforms to a selected frame range
  • Video thumbnail extraction - extract a frame from MP4, MKV, AVI and pass it through the normal transform pipeline
  • Multi-tier cache - L1 in-memory (moka) + L2 disk with singleflight dedup
  • Security - domain allowlist, optional HMAC request signing, source URL encryption (AES-CBC), configurable CORS origins, SSRF protection (private IP blocking, per-hop allowlist re-validation on redirects), input/output/transform disallow lists.

Request Styles

Path-style

GET /{transforms}/{image-url}

Transforms are comma-separated tokens before the image URL:

# Resize to 300x200
GET /300x200/https://example.com/photo.jpg
# Resize and convert to WebP
GET /300x200,webp/https://example.com/photo.jpg
# Grayscale only
GET /,grayscale/https://example.com/photo.jpg
# Grayscale and blur
GET /,grayscale,blur:0.8/https://example.com/photo.jpg

Query-style

GET /proxy?url=https://example.com/photo.jpg&w=300&h=200&format=webp

Query params take precedence when both styles are combined.

Transform Parameters

ParamValuesDescription
wintegerOutput width in pixels
hintegerOutput height in pixels
fitcontain (default), coverResize mode
formatjpeg, png, webp, avif, jxl, gif, bmp, tiff, ico, bestOutput format; best encodes in multiple formats and returns the smallest result
q1-100 (default: 85)Compression quality
rotate90, 180, 270Rotation degrees
fliph, vFlip horizontal or vertical
grayscaletrueConvert to grayscale
bright-100 to 100Brightness adjustment
contrast-100 to 100Contrast adjustment
blurfloat (sigma)Gaussian blur
wmURLWatermark image URL
seek5.0, 0.5r, auto (default: 0)Video seek: absolute seconds, relative ratio, or auto (middle frame)
gif_animall, N, N-M, -NAnimated GIF: output all frames, apply transforms starting at frame N, to frame range N-M, or to last N frames
gif_aftrue, 1GIF all-frames: output all frames, not just the gif_anim range; style transforms apply only to frames in range, geometric transforms (resize, rotate, flip) apply to all frames
sigstringHMAC-SHA256 signature (if required)

API Endpoints

MethodPathDescription
GET/healthHealth check with cache stats
GET/proxyQuery-style image proxy
GET/*pathPath-style image proxy

Getting Started

Linux / macOS

curl -s -o- https://raw.githubusercontent.com/ViGrise/previewproxy/main/install.sh | sudo -E bash
wget -qO- https://raw.githubusercontent.com/ViGrise/previewproxy/main/install.sh | sudo -E bash

Installs to /usr/local/bin. Override with env vars:

VarDefaultDescription
INSTALL_DIR/usr/local/binDestination directory
VERSIONlatestRelease tag, e.g. v1.0.0

Windows

irm https://raw.githubusercontent.com/ViGrise/previewproxy/main/install.ps1 | iex

Installs to %LOCALAPPDATA%\previewproxy\bin and adds it to your user PATH. Override with flags:

FlagDefaultDescription
-InstallDir%LOCALAPPDATA%\previewproxy\binDestination directory
-VersionlatestRelease tag, e.g. v1.0.0

Docker

docker run -d -p 8080:8080 \
-e PP_ALLOWED_HOSTS=img.example.com \
-e PP_HMAC_KEY=mysecret \
ghcr.io/vigrise/previewproxy:latest

Or with Docker Compose:

curl -O https://raw.githubusercontent.com/ViGrise/previewproxy/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/ViGrise/previewproxy/main/.env.sample
cp .env.sample .env
# Edit .env as needed
docker-compose up -d

Upgrade

To be upgrade to latest version, you can rerun the installation script or use the following command:

previewproxy upgrade

CLI Reference

Configuration is read from environment variables (.env file) or CLI flags - CLI flags take precedence. See Environment Variables for full reference.


Security

Upstream Allowlist

Set PP_ALLOWED_HOSTS to a comma-separated list of trusted upstream domains. Wildcards match a single label:

PP_ALLOWED_HOSTS=img.example.com,*.cdn.example.com

Leave empty to allow any host (open mode - use only in trusted environments).

HMAC Signing

Set PP_HMAC_KEY to require signed requests. The signature is computed as:

HMAC-SHA256(key, canonical_string)

where canonical_string is alphabetically sorted key=value pairs (excluding sig) joined by &, followed by : and the decoded image URL. Encode the result as URL-safe base64 (no padding) and pass it as the sig parameter.

CORS

Set PP_CORS_ALLOW_ORIGIN to restrict which browser origins may access the proxy. Wildcards match a single subdomain label:

PP_CORS_ALLOW_ORIGIN=https://app.example.com,*.cdn.example.com

Leave as * (default) to allow any origin.

Formats & Transforms Disallow Lists

Block specific input formats, output formats, or transforms via env vars. Each accepts a comma-separated list of tokens; unknown tokens are ignored with a warning.

Input formats (PP_INPUT_DISALLOW_LIST) - reject requests whose source image matches a blocked format (returns 400):

# Block video thumbnails and PDF inputsPP_INPUT_DISALLOW_LIST=video,pdf

Tokens: jpeg, png, gif, webp, avif, jxl, bmp, tiff, pdf, psd, video

Output formats (PP_OUTPUT_DISALLOW_LIST) - reject requests that would produce a blocked format (returns 400). Defaults to empty (allow all):

# Allow all output formatsPP_OUTPUT_DISALLOW_LIST=

Tokens: jpeg, png, gif, webp, avif, jxl, bmp, tiff, ico

Transforms (PP_TRANSFORM_DISALLOW_LIST) - reject requests that apply a blocked transform (returns 400). Defaults to empty (allow all):

# Block watermarking and animated GIF processing onlyPP_TRANSFORM_DISALLOW_LIST=watermark,gif_anim

Tokens: resize, rotate, flip, grayscale, brightness, contrast, blur, watermark, gif_anim

Note: Input disallow checks are not applied to sources with no Content-Type header (e.g. S3 objects without metadata), as the format cannot be determined before fetching.

SSRF Protection

Private, loopback, link-local, and reserved IP ranges (RFC 1918, RFC 6598, IPv6 ULA) are always blocked. On redirects, each hop's resolved IP and host are re-validated before following, preventing bypass via open redirectors.

Source URL Encryption

Set PP_SOURCE_URL_ENCRYPTION_KEY to hide upstream origins from request logs and CDN traces. Encrypt URLs with AES-CBC before putting them in requests; previewproxy decrypts server-side before fetching.

Configure the key (hex-encoded, 32/48/64 hex chars for AES-128/192/256):

# Generate a key
openssl rand -hex 32
PP_SOURCE_URL_ENCRYPTION_KEY=1eb5b0e971ad7f45324c1bb15c947cb207c43152fa5c6c7f35c4f36e0c18e0f1

Encrypt a URL using the built-in CLI helper:

previewproxy encrypt-url "https://cdn.example.com/photo.jpg" [--key <hex-key>]
# stdout: <encrypted-blob> (pipe-friendly)# stderr: /enc/<encrypted-blob> (ready-to-use path)

Use encrypted URLs in requests:

# Path-style
GET /300x200,webp/enc/<encrypted-blob># Query-style
GET /proxy?url=<encrypted-blob>&enc=1&w=300&h=200

The enc query flag (any value, or just its presence) signals that url is encrypted.

Note: The same source URL always produces the same encrypted blob (deterministic IV). This is intentional for CDN cache-hit compatibility. Rotate your key if you need to invalidate existing blobs.

Recommended: Use with PP_HMAC_KEY signing to prevent padding oracle attacks on encrypted URLs.

URL Aliases

Map short scheme names to real HTTP base URLs to avoid exposing domains in requests:

PP_URL_ALIASES=mycdn=https://img.example.com,cdn2=https://other.com

Then use the alias scheme in path-style or query-style requests:

# Path-style with alias
GET /300x200,webp/mycdn:/photos/dog.jpg
# Query-style with alias
GET /proxy?url=mycdn:/photos/dog.jpg&w=300
  • Aliases bypass PP_ALLOWED_HOSTS and private IP blocking.
  • Base URL must be http:// or https://

Development

Requirements

Runtime

  • ffmpeg + ffprobe - video frame extraction and duration probing (apt install ffmpeg / brew install ffmpeg)

Build-time native libs

  • libheif - HEIF/HEIC image support
  • libjxl - JPEG XL support
  • libdav1d - AV1 decoder (used by libheif/libjxl)
  • pkg-config, cmake, nasm - build toolchain
# Ubuntu / Debian
sudo apt install ffmpeg pkg-config cmake nasm libheif-dev libjxl-dev libdav1d-dev
# macOS
brew install ffmpeg pkg-config cmake nasm libheif jpeg-xl dav1d

Commands

# Start dev server
cargo run
# Or with watch mode
cargo watch -q -x run
# Run tests
cargo test# Format
cargo fmt
# Lint
cargo clippy

Contributing

Contributions are welcome. Please open an issue or pull request at github.com/ViGrise/previewproxy.

License

Apache 2.0 - see LICENSE.

About

PreviewProxy is a standalone Axum-based HTTP image proxy service written in Rust. It fetches remote images by HTTP URLs, S3 buckets, or local storage, applies on-the-fly transformations (resize, crop, format conversion, blur, watermark, etc.), and returns optimized images to the client.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages