Skip to content

ViGrise/previewproxy

Repository files navigation

PreviewProxy

Latest Release CI Rust Axum License: Apache 2.0 GitHub Container Registry GitHub stars GitHub 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

Param Values Description
w integer Output width in pixels
h integer Output height in pixels
fit contain (default), cover Resize mode
format jpeg, png, webp, avif, jxl, gif, bmp, tiff, ico, best Output format; best encodes in multiple formats and returns the smallest result
q 1-100 (default: 85) Compression quality
rotate 90, 180, 270 Rotation degrees
flip h, v Flip horizontal or vertical
grayscale true Convert to grayscale
bright -100 to 100 Brightness adjustment
contrast -100 to 100 Contrast adjustment
blur float (sigma) Gaussian blur
wm URL Watermark image URL
seek 5.0, 0.5r, auto (default: 0) Video seek: absolute seconds, relative ratio, or auto (middle frame)
gif_anim all, N, N-M, -N Animated GIF: output all frames, apply transforms starting at frame N, to frame range N-M, or to last N frames
gif_af true, 1 GIF 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
sig string HMAC-SHA256 signature (if required)

API Endpoints

Method Path Description
GET /health Health check with cache stats
GET /proxy Query-style image proxy
GET /*path Path-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:

Var Default Description
INSTALL_DIR /usr/local/bin Destination directory
VERSION latest Release 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:

Flag Default Description
-InstallDir %LOCALAPPDATA%\previewproxy\bin Destination directory
-Version latest Release 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 inputs
PP_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 formats
PP_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 only
PP_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 (operator-controlled, implicitly trusted)
  • SSRF protection (private IP blocking) still applies
  • 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

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages