Server: - Add a --cache-path allowlist so a server can be limited to the caches it is meant to purge, defaulting to any path as before. - Set socket permissions explicitly (--socket-mode, default 0660) instead of inheriting the service manager's umask, which left the socket unreachable. - Refuse to remove a socket another instance is still serving. - Read keys both raw and decoded, so keys nginx stored with escapes and keys a caller escaped by hand both purge. - Add an exact= parameter for literal keys containing glob punctuation. - Report purge failures as 500 rather than 502, and send error bodies through http.Error so a failure is not reported as a successful purge. - Graceful shutdown with systemd readiness notification. Purge: - Group purge arguments into PurgeRequest and report the number of entries removed. - Compile exclude globs once, and fail the purge when one is invalid rather than purging the keys it was meant to keep. - Cap header scanning and tolerate entries nginx evicts mid-walk. - Switch to filepath.WalkDir to avoid an Lstat per cache file. New: - service command to install, start, stop, and remove the system service. - service install takes --cache-path, writing the allowlist into the unit it installs, so an installed service is restricted from its first start. - Makefile, VERSION, and build identifiers stamped via ldflags. - Tests for the server handler and the service command. Build: - Update to Go 1.25, kong v1, GoReleaser v2, and current GitHub Actions. - Add vet and test steps to CI. - Rename purgeCmd.go/serverCmd.go to Go's file naming convention. Bump version to 0.2.0.
267 lines
8.8 KiB
Markdown
267 lines
8.8 KiB
Markdown
# nginx-cache-purge
|
|
A tool to help purge Nginx cache. It can either run locally with the purge command, or run as a local unix service to allow for purging by Nginx http requests. The tool supports using wildcard/glob syntax in the purge key to match multiple keys from the cache.
|
|
|
|
## Install
|
|
You can install either by downloading the latest binary release, or by building.
|
|
|
|
## Building
|
|
Building should be as simple as running:
|
|
|
|
```
|
|
make
|
|
```
|
|
|
|
## Usage
|
|
The following are some examples of ways to purge cache
|
|
|
|
### Purge a specific key
|
|
```
|
|
$ nginx-cache-purge purge /var/nginx/proxy_temp/cache example.com/index.html
|
|
```
|
|
|
|
### Purge all keys for a domain
|
|
```
|
|
$ nginx-cache-purge purge /var/nginx/proxy_temp/cache 'example.com/*'
|
|
```
|
|
|
|
### Purge all keys for jpeg and png files
|
|
```
|
|
$ nginx-cache-purge purge /var/nginx/proxy_temp/cache 'example.com/*.{jpg,jpeg,png}'
|
|
```
|
|
|
|
### Purge all keys
|
|
```
|
|
$ nginx-cache-purge purge /var/nginx/proxy_temp/cache '*'
|
|
```
|
|
|
|
### Purge a key that contains wildcard characters
|
|
Whether a key is a wildcard is otherwise guessed from the punctuation in it, and
|
|
real cache keys carry that punctuation: a request URI with a query string puts
|
|
`?` in the key, and PHP-style array parameters put `[` and `]`. `--exact` says
|
|
the key is a literal, for both the key and any excludes.
|
|
|
|
```
|
|
$ nginx-cache-purge purge --exact /var/nginx/proxy_temp/cache 'example.com/list.php?f[]=x'
|
|
```
|
|
|
|
## Running as a service
|
|
If you want to run as a service to allow purge requests via http requests, the
|
|
service can install itself:
|
|
|
|
```
|
|
nginx-cache-purge service install --cache-path /var/nginx/proxy_temp/cache
|
|
nginx-cache-purge service start
|
|
```
|
|
|
|
`service` also accepts `stop`, `restart`, `status`, and `uninstall`. The
|
|
installed unit runs `nginx-cache-purge server` as a notify service, creates the
|
|
runtime directory the socket lives in, and restarts on failure. `--cache-path`
|
|
restricts which caches that server will purge, and is covered below.
|
|
|
|
Connecting to a UNIX socket needs write permission on it, so the socket is
|
|
created mode `0660`: the purge server and Nginx have to run as the same user, or
|
|
share a group. The installed unit runs as root, so either add a
|
|
`User=`/`Group=` drop-in for it, or widen the socket with
|
|
`--socket-mode`.
|
|
|
|
### Restricting which caches may be purged
|
|
The directory to purge comes from the request, and the purge deletes what it
|
|
finds under it, so a server left unrestricted will purge any path its user can
|
|
reach. Pass `--cache-path` to name the caches it may serve, repeating it for
|
|
more than one:
|
|
|
|
```
|
|
nginx-cache-purge server --cache-path /var/nginx/proxy_temp/cache
|
|
```
|
|
|
|
A request naming any other directory is refused with `403`. Directories inside a
|
|
named cache are allowed, and symlinks are resolved, so a link to a named cache is
|
|
recognised as that cache. Naming no path leaves every path purgeable, which is
|
|
what a server without the flag has always done.
|
|
|
|
`service install` takes the same flag and writes it into the unit's `ExecStart`,
|
|
so an installed service is restricted from its first start:
|
|
|
|
```
|
|
nginx-cache-purge service install --cache-path /var/nginx/proxy_temp/cache
|
|
```
|
|
|
|
Repeat the flag for more than one cache. Paths are made absolute at install, as
|
|
the unit runs from a working directory of the service manager's choosing;
|
|
symlinks are left as written and resolved per request. The allowlist lives in the
|
|
unit, so changing it means installing again: `service uninstall` then
|
|
`service install` with the paths you want.
|
|
|
|
## Nginx config
|
|
If you want to purge via Nginx http requests, you'll need to add configuration to your Nginx config file.
|
|
|
|
The server reads four query parameters:
|
|
|
|
| Parameter | Description |
|
|
| --- | --- |
|
|
| `path` | Path to the cache directory, the same one given to `proxy_cache_path`. |
|
|
| `key` | Cache key or wildcard match, the same one built by `proxy_cache_key`. |
|
|
| `exclude` | Key to keep, can be a wildcard. Repeat it to exclude more than one. |
|
|
| `exact` | Read the key and excludes as literals rather than wildcards. |
|
|
|
|
Nginx substitutes `$request_uri` into the rewrite without escaping it, so a key
|
|
containing `%` or `+` arrives as the literal bytes Nginx stored it under, and
|
|
that is what is purged. A key escaped by a caller writing the request itself is
|
|
purged too, so either convention works.
|
|
|
|
### Literal keys and wildcards
|
|
A key built from `$request_uri` is a literal, and a request URI routinely holds
|
|
the punctuation that would otherwise mark the key as a wildcard: `?` from a query
|
|
string, `[` and `]` from PHP-style array parameters. Read as wildcards those keys
|
|
purge the wrong entries, fail outright, or match nothing while still answering
|
|
`PURGED` and leaving the stale entry served. The examples below therefore pass
|
|
`exact=1`, which is what you want when a PURGE request names one URL.
|
|
|
|
Leave `exact` off where the purge is meant to take a wildcard, such as the
|
|
`/purge(/.*)` location further down, where a request for `/purge/images/*`
|
|
clears everything under `/images/`.
|
|
|
|
Because `$request_uri` carries the client's own query string into the purge
|
|
parameters, put `exact=1` **before** `key=` in the rewrite. Parameters are taken
|
|
first-wins, so an `exact=0` a client appends to its request URI arrives second
|
|
and is ignored. The same ordering already protects `path`. Note that `exclude` is
|
|
collected rather than taken first-wins, so a client can append an `exclude` that
|
|
holds back its own purge.
|
|
|
|
### Map PURGE requests
|
|
```
|
|
http {
|
|
map $request_method $is_purge {
|
|
default 0;
|
|
PURGE 1;
|
|
}
|
|
|
|
proxy_cache_path /var/nginx/proxy_temp/cache levels=1:2 keys_zone=my_cache:10m;
|
|
proxy_cache_key $server_name$request_uri;
|
|
|
|
server {
|
|
location / {
|
|
proxy_cache_bypass $is_purge;
|
|
if ($is_purge) {
|
|
proxy_pass http://unix:/run/nginx-cache-purge/http.sock;
|
|
rewrite ^ /?path=/var/nginx/proxy_temp/cache&exact=1&key=$server_name$request_uri break;
|
|
}
|
|
|
|
proxy_cache my_cache;
|
|
proxy_pass http://upstream;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Auth via cookie
|
|
```
|
|
http {
|
|
map $cookie_purge_token $is_purge {
|
|
default 0;
|
|
nnCgKUx1p2bIABXR 1;
|
|
}
|
|
|
|
proxy_cache_path /var/nginx/proxy_temp/cache levels=1:2 keys_zone=my_cache:10m;
|
|
proxy_cache_key $server_name$request_uri;
|
|
|
|
server {
|
|
location / {
|
|
proxy_cache_bypass $is_purge;
|
|
if ($is_purge) {
|
|
proxy_pass http://unix:/run/nginx-cache-purge/http.sock;
|
|
rewrite ^ /?path=/var/nginx/proxy_temp/cache&exact=1&key=$server_name$request_uri break;
|
|
}
|
|
|
|
proxy_cache my_cache;
|
|
proxy_pass http://upstream;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Auth via header
|
|
```
|
|
http {
|
|
map $http_purge_token $is_purge {
|
|
default 0;
|
|
nnCgKUx1p2bIABXR 1;
|
|
}
|
|
|
|
proxy_cache_path /var/nginx/proxy_temp/cache levels=1:2 keys_zone=my_cache:10m;
|
|
proxy_cache_key $server_name$request_uri;
|
|
|
|
server {
|
|
location / {
|
|
proxy_cache_bypass $is_purge;
|
|
if ($is_purge) {
|
|
proxy_pass http://unix:/run/nginx-cache-purge/http.sock;
|
|
rewrite ^ /?path=/var/nginx/proxy_temp/cache&exact=1&key=$server_name$request_uri break;
|
|
}
|
|
|
|
proxy_cache my_cache;
|
|
proxy_pass http://upstream;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Auth via header and IP white list.
|
|
```
|
|
http {
|
|
map $http_purge_token $is_purge {
|
|
default 0;
|
|
nnCgKUx1p2bIABXR 1;
|
|
}
|
|
|
|
geo $purge_allowed {
|
|
default 0;
|
|
127.0.0.1 1;
|
|
192.168.0.0/24 1;
|
|
}
|
|
|
|
proxy_cache_path /var/nginx/proxy_temp/cache levels=1:2 keys_zone=my_cache:10m;
|
|
proxy_cache_key $server_name$request_uri;
|
|
|
|
server {
|
|
location / {
|
|
set $should_purge $purge_allowed;
|
|
if ($is_purge != 1) {
|
|
set $should_purge 0;
|
|
}
|
|
proxy_cache_bypass $should_purge;
|
|
if ($should_purge) {
|
|
proxy_pass http://unix:/run/nginx-cache-purge/http.sock;
|
|
rewrite ^ /?path=/var/nginx/proxy_temp/cache&exact=1&key=$server_name$request_uri break;
|
|
}
|
|
|
|
proxy_cache my_cache;
|
|
proxy_pass http://upstream;
|
|
}
|
|
}
|
|
}
|
|
```
|
|
|
|
### Using IP whitelists
|
|
This location takes a wildcard, so it leaves `exact` off: a request for
|
|
`/purge/images/*` clears everything under `/images/`.
|
|
|
|
```
|
|
http {
|
|
proxy_cache_path /var/nginx/proxy_temp/cache levels=1:2 keys_zone=my_cache:10m;
|
|
proxy_cache_key $server_name$request_uri;
|
|
|
|
server {
|
|
location / {
|
|
proxy_cache my_cache;
|
|
proxy_pass http://upstream;
|
|
}
|
|
location ~ /purge(/.*) {
|
|
allow 127.0.0.1;
|
|
deny all;
|
|
proxy_pass http://unix:/run/nginx-cache-purge/http.sock;
|
|
rewrite ^ /?path=/var/nginx/proxy_temp/cache&key=$server_name$1 break;
|
|
}
|
|
}
|
|
}
|
|
```
|