Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b88b6251f9 | |||
| 2b05c4e544 | |||
| 4167409ccf | |||
| 07572f8e6c | |||
| da68d078b0 | |||
| 9d48ce54c1 | |||
| c120d7a164 | |||
| 6aa7c63b07 | |||
| 05c4e86f4b | |||
| df77ee5498 | |||
| 5ecb74ad78 |
24
Dockerfile
24
Dockerfile
@@ -1,15 +1,21 @@
|
||||
FROM nginx:alpine
|
||||
FROM openresty/openresty:alpine
|
||||
|
||||
# Copy nginx configuration and HTML
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
# Copy config (OpenResty uses this path)
|
||||
COPY nginx.conf /usr/local/openresty/nginx/conf/nginx.conf
|
||||
# HTML and data dirs (config references /usr/share/nginx/html and /etc/nginx/data)
|
||||
RUN mkdir -p /usr/share/nginx/html /etc/nginx/data/cache /etc/nginx/data/temp
|
||||
COPY index.html /usr/share/nginx/html/index.html
|
||||
|
||||
# Create data directories
|
||||
RUN mkdir -p /etc/nginx/data/cache /etc/nginx/data/temp && \
|
||||
chown -R nginx:nginx /etc/nginx/data
|
||||
# When volume is mounted at /etc/nginx/data, ensure cache/temp exist and are writable (fix rename Permission denied)
|
||||
RUN echo '#!/bin/sh' > /docker-entrypoint.sh \
|
||||
&& echo 'set -e' >> /docker-entrypoint.sh \
|
||||
&& echo 'mkdir -p /etc/nginx/data/cache /etc/nginx/data/temp' >> /docker-entrypoint.sh \
|
||||
&& echo 'chown -R nobody:nobody /etc/nginx/data 2>/dev/null || true' >> /docker-entrypoint.sh \
|
||||
&& echo 'chmod -R 755 /etc/nginx/data' >> /docker-entrypoint.sh \
|
||||
&& echo 'exec "$@"' >> /docker-entrypoint.sh \
|
||||
&& chmod +x /docker-entrypoint.sh
|
||||
|
||||
# Expose port
|
||||
EXPOSE 3000
|
||||
|
||||
# Start nginx
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["/usr/local/openresty/bin/openresty", "-g", "daemon off;"]
|
||||
|
||||
@@ -20,21 +20,12 @@ curl "http://localhost:3000/?url=https://api.example.com/data"
|
||||
|
||||
# Skip cache for this request
|
||||
curl -H "X-NoCache: 1" "http://localhost:3000/?url=https://api.example.com/data"
|
||||
|
||||
# Cache custom status codes (e.g., 400s)
|
||||
curl -H "X-Status: 400" "http://localhost:3000/?url=https://api.example.com/bad"
|
||||
|
||||
# Purge cache for specific URL
|
||||
curl "http://localhost:3000/purge/?url=https://api.example.com/data"
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
- **Always caches** 2xx status codes for 1 year
|
||||
- Cache size: 100GB max
|
||||
- Request size: 128MB max
|
||||
- X-Cache-Status header shows cache status
|
||||
- X-NoCache header bypasses cache
|
||||
- X-Status header caches custom status codes
|
||||
- Interactive web interface at root path
|
||||
- Cache purge endpoint
|
||||
|
||||
147
index.html
147
index.html
@@ -3,85 +3,132 @@
|
||||
<head>
|
||||
<title>Nginx Cache Proxy</title>
|
||||
<style>
|
||||
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
|
||||
.usage { background: #f5f5f5; padding: 15px; border-radius: 5px; margin: 10px 0; }
|
||||
.copy-field { margin: 20px 0; }
|
||||
input { width: 100%; padding: 10px; font-size: 16px; }
|
||||
button { background: #007cba; color: white; border: none; padding: 10px 20px; cursor: pointer; }
|
||||
button:hover { background: #005a87; }
|
||||
.status { margin-top: 10px; color: green; }
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--text: #333333;
|
||||
--heading: #007cba;
|
||||
--border: #007cba;
|
||||
--code-bg: #f4f4f4;
|
||||
--code-inline-bg: #f4f4f4;
|
||||
--pre-bg: #2d2d2d;
|
||||
--pre-text: #f8f8f2;
|
||||
--copy-bg: #e8f4fd;
|
||||
--copy-border: #007cba;
|
||||
--success: #28a745;
|
||||
--error: #dc3545;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--bg: #1a1a1a;
|
||||
--text: #e0e0e0;
|
||||
--heading: #58a6ff;
|
||||
--border: #58a6ff;
|
||||
--code-bg: #2d2d2d;
|
||||
--code-inline-bg: #3d3d3d;
|
||||
--pre-bg: #0d0d0d;
|
||||
--copy-bg: #1c3d5e;
|
||||
--copy-border: #58a6ff;
|
||||
--success: #3fb950;
|
||||
--error: #f85149;
|
||||
}
|
||||
}
|
||||
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; line-height: 1.6; color: var(--text); background: var(--bg); }
|
||||
h1 { border-bottom: 2px solid var(--border); padding-bottom: 10px; color: var(--text); }
|
||||
h2 { color: var(--heading); margin-top: 30px; }
|
||||
h3 { color: var(--text); opacity: 0.8; }
|
||||
code { background: var(--code-inline-bg); padding: 2px 6px; border-radius: 3px; font-family: "Consolas", "Monaco", monospace; font-size: 0.9em; color: var(--text); }
|
||||
pre { background: var(--pre-bg); color: var(--pre-text); padding: 15px; border-radius: 5px; overflow-x: auto; }
|
||||
pre code { background: none; padding: 0; color: inherit; }
|
||||
.copy-section { background: var(--copy-bg); padding: 20px; border-radius: 8px; margin: 20px 0; border-left: 4px solid var(--copy-border); }
|
||||
.copy-section h3 { margin-top: 0; color: var(--text); }
|
||||
.copy-section p { color: var(--text); }
|
||||
button { background: var(--heading); color: white; border: none; padding: 12px 24px; cursor: pointer; border-radius: 5px; font-size: 14px; font-weight: 600; }
|
||||
button:hover { filter: brightness(0.9); }
|
||||
.status { margin-top: 10px; font-size: 14px; }
|
||||
.status.success { color: var(--success); }
|
||||
.status.error { color: var(--error); }
|
||||
ul { line-height: 1.8; }
|
||||
li { color: var(--text); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Nginx Cache Proxy</h1>
|
||||
<p>Proxy and cache any HTTP/HTTPS URL with automatic caching.</p>
|
||||
|
||||
<div class="copy-field">
|
||||
<h3>Quick URL Builder</h3>
|
||||
<input type="text" id="urlInput" placeholder="Enter target URL (e.g., https://api.example.com/data)">
|
||||
<button onclick="copyUrl()">Copy Proxy URL</button>
|
||||
<div class="copy-section">
|
||||
<h3>Quick Copy</h3>
|
||||
<p>Copy this proxy URL with <code>?url=</code> appended, then paste any URL after it:</p>
|
||||
<button onclick="copyProxyUrl()">Copy Proxy URL</button>
|
||||
<div id="status" class="status"></div>
|
||||
</div>
|
||||
|
||||
<h2>Usage</h2>
|
||||
<h2>Usage Examples</h2>
|
||||
|
||||
<div class="usage">
|
||||
<h3>Basic Proxy</h3>
|
||||
<pre>curl "http://localhost:3000/?url=https://api.example.com/data"</pre>
|
||||
</div>
|
||||
<h3>Basic Proxy (Cached)</h3>
|
||||
<pre><code id="basicExample">Loading...</code></pre>
|
||||
|
||||
<div class="usage">
|
||||
<h3>Skip Cache</h3>
|
||||
<pre>curl -H "X-NoCache: 1" "http://localhost:3000/?url=https://api.example.com/data"</pre>
|
||||
</div>
|
||||
|
||||
<div class="usage">
|
||||
<h3>Cache Custom Status Codes</h3>
|
||||
<pre>curl -H "X-Status: 400" "http://localhost:3000/?url=https://api.example.com/bad"</pre>
|
||||
</div>
|
||||
|
||||
<div class="usage">
|
||||
<h3>Purge Cache</h3>
|
||||
<pre>curl "http://localhost:3000/purge/?url=https://api.example.com/data"</pre>
|
||||
</div>
|
||||
<h3>Skip Cache</h3>
|
||||
<pre><code id="nocacheExample">Loading...</code></pre>
|
||||
|
||||
<h2>Features</h2>
|
||||
<ul>
|
||||
<li>Always caches 2xx status codes for 1 year</li>
|
||||
<li>100GB max cache size</li>
|
||||
<li>128MB max per request</li>
|
||||
<li>X-Cache-Status header shows hit/miss</li>
|
||||
<li>X-NoCache header bypasses cache</li>
|
||||
<li>X-Status header caches custom status codes</li>
|
||||
<li>Purge endpoint for cache management</li>
|
||||
<li>Always caches <strong>2xx</strong> status codes for <strong>1 year</strong></li>
|
||||
<li>Cache size: <strong>100GB</strong> max</li>
|
||||
<li><code>X-Cache-Status</code> header shows cache hit/miss</li>
|
||||
<li><code>X-NoCache</code> header bypasses cache</li>
|
||||
<li>Interactive web interface</li>
|
||||
</ul>
|
||||
|
||||
<script>
|
||||
function copyUrl() {
|
||||
const input = document.getElementById('urlInput');
|
||||
const status = document.getElementById('status');
|
||||
function getBaseUrl() {
|
||||
const protocol = window.location.protocol;
|
||||
const hostname = window.location.hostname;
|
||||
const port = window.location.port;
|
||||
let baseUrl = protocol + '//' + hostname;
|
||||
|
||||
if (!input.value.trim()) {
|
||||
status.textContent = 'Please enter a URL first';
|
||||
status.style.color = 'red';
|
||||
return;
|
||||
if ((protocol === 'http:' && port !== '80' && port !== '') ||
|
||||
(protocol === 'https:' && port !== '443' && port !== '')) {
|
||||
baseUrl += ':' + port;
|
||||
}
|
||||
return baseUrl;
|
||||
}
|
||||
|
||||
const proxyUrl = window.location.origin + '/?url=' + encodeURIComponent(input.value);
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const base = getBaseUrl();
|
||||
|
||||
document.getElementById('basicExample').textContent =
|
||||
'curl "' + base + '/?url=https://api.example.com/data"';
|
||||
|
||||
document.getElementById('nocacheExample').textContent =
|
||||
'curl -H "X-NoCache: 1" "' + base + '/?url=https://api.example.com/data"';
|
||||
});
|
||||
|
||||
function copyProxyUrl() {
|
||||
const proxyUrl = window.location.href + '?url=';
|
||||
const statusEl = document.getElementById('status');
|
||||
|
||||
navigator.clipboard.writeText(proxyUrl).then(() => {
|
||||
status.textContent = 'Proxy URL copied to clipboard!';
|
||||
status.style.color = 'green';
|
||||
statusEl.className = 'status success';
|
||||
statusEl.textContent = 'Copied: ' + proxyUrl;
|
||||
}).catch(err => {
|
||||
// Fallback for older browsers
|
||||
const textArea = document.createElement('textarea');
|
||||
textArea.value = proxyUrl;
|
||||
textArea.style.position = 'fixed';
|
||||
textArea.style.opacity = '0';
|
||||
document.body.appendChild(textArea);
|
||||
textArea.select();
|
||||
document.execCommand('copy');
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
statusEl.className = 'status success';
|
||||
statusEl.textContent = 'Copied: ' + proxyUrl;
|
||||
} catch (e) {
|
||||
statusEl.className = 'status error';
|
||||
statusEl.textContent = 'Copy failed. URL: ' + proxyUrl;
|
||||
}
|
||||
document.body.removeChild(textArea);
|
||||
status.textContent = 'Proxy URL copied to clipboard!';
|
||||
status.style.color = 'green';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
107
nginx.conf
107
nginx.conf
@@ -12,35 +12,122 @@ http {
|
||||
proxy_cache_path /etc/nginx/data/cache levels=1:2 keys_zone=api_cache:10m max_size=100g inactive=365d;
|
||||
|
||||
error_log /dev/stdout warn;
|
||||
access_log /dev/stdout;
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" cache:$upstream_cache_status';
|
||||
access_log /dev/stdout main;
|
||||
|
||||
resolver 127.0.0.11 valid=60s;
|
||||
|
||||
# Raw url= value from request (stops at next & so %26 in encoded URLs is preserved)
|
||||
map $request_uri $url_encoded {
|
||||
default "";
|
||||
"~*[?&]url=((?:[^&%]|%[0-9A-Fa-f][0-9A-f])*)(?:&|$)" $1;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 3000;
|
||||
|
||||
location / {
|
||||
# If no url parameter, serve the readme
|
||||
if ($arg_url = "") {
|
||||
rewrite ^ /index.html last;
|
||||
# Return immediately for OPTIONS so cache/proxy are never involved (avoids "if" affecting cache)
|
||||
if ($request_method = 'OPTIONS') {
|
||||
add_header Access-Control-Allow-Origin *;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH";
|
||||
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Cache,X-Cache-Purge,X-Status";
|
||||
add_header Access-Control-Max-Age 86400;
|
||||
return 204;
|
||||
}
|
||||
|
||||
proxy_pass $arg_url;
|
||||
set $backend_base "";
|
||||
set $body_for_cache_key "";
|
||||
# Decode url, build upstream URL, strip our "url=". Read body for cache key (so POST is keyed by body).
|
||||
rewrite_by_lua_block {
|
||||
ngx.req.read_body()
|
||||
local body = ngx.req.get_body_data()
|
||||
if not body and ngx.req.get_body_file() then
|
||||
local f = io.open(ngx.req.get_body_file(), "rb")
|
||||
if f then body = f:read("*a"); f:close() end
|
||||
end
|
||||
ngx.var.body_for_cache_key = ngx.md5(body or "")
|
||||
local enc = ngx.var.url_encoded
|
||||
local decoded = (enc and enc ~= "") and ngx.unescape_uri(enc) or ngx.var.arg_url or ""
|
||||
if decoded == "" then
|
||||
ngx.exec("/index.html")
|
||||
return
|
||||
end
|
||||
local args = ngx.var.args or ""
|
||||
local rest = args:gsub("^url=[^&]*&?", ""):gsub("&url=[^&]*", ""):gsub("^url=[^&]*$", "")
|
||||
local full = decoded
|
||||
if rest ~= "" then
|
||||
local sep = decoded:find("?") and "&" or "?"
|
||||
full = decoded .. sep .. rest
|
||||
end
|
||||
local scheme, host, pathquery = full:match("^(https?)://([^/]+)(.*)$")
|
||||
if not host then
|
||||
ngx.status = 400
|
||||
ngx.say("invalid url")
|
||||
return ngx.exit(400)
|
||||
end
|
||||
if pathquery == "" then pathquery = "/" end
|
||||
local path = pathquery:match("^([^?]*)") or "/"
|
||||
local query = pathquery:match("%?(.*)$") or ""
|
||||
ngx.var.backend_base = scheme .. "://" .. host
|
||||
ngx.req.set_uri(path)
|
||||
ngx.req.set_uri_args((query:gsub("^%?", "")))
|
||||
}
|
||||
proxy_pass $backend_base$uri$is_args$args;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $proxy_host;
|
||||
proxy_ssl_server_name on;
|
||||
|
||||
proxy_cache api_cache;
|
||||
proxy_cache_valid 200 201 202 203 204 205 206 207 208 226 365d;
|
||||
# Cache key is built from variables; must fit in proxy_buffer_size (body part is MD5 in Lua)
|
||||
proxy_buffer_size 16k;
|
||||
proxy_buffering on;
|
||||
proxy_buffers 4 256k;
|
||||
proxy_busy_buffers_size 256k;
|
||||
proxy_max_temp_file_size 1024m;
|
||||
|
||||
proxy_cache_bypass $http_x_nocache;
|
||||
proxy_no_cache $http_x_nocache;
|
||||
# Strip upstream CORS and Set-Cookie so we only send our own CORS and don't leak cookies
|
||||
proxy_hide_header Access-Control-Allow-Origin;
|
||||
proxy_hide_header Set-Cookie;
|
||||
proxy_hide_header Access-Control-Allow-Methods;
|
||||
proxy_hide_header Access-Control-Allow-Headers;
|
||||
proxy_hide_header Access-Control-Expose-Headers;
|
||||
proxy_hide_header Access-Control-Max-Age;
|
||||
|
||||
# CORS headers — replace with our own *
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
add_header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS, HEAD, PATCH" always;
|
||||
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization,X-Cache,X-Cache-Purge,X-Status" always;
|
||||
add_header Access-Control-Expose-Headers "X-Cache-Status" always;
|
||||
|
||||
proxy_cache api_cache;
|
||||
proxy_cache_methods GET HEAD POST;
|
||||
proxy_cache_key $backend_base$request_method$uri$is_args$args$body_for_cache_key;
|
||||
proxy_cache_valid 200 201 202 203 204 205 206 207 208 226 365d;
|
||||
# Ignore headers that would prevent storing the response
|
||||
proxy_ignore_headers Cache-Control Expires Set-Cookie Vary X-Accel-Expires X-Accel-Redirect;
|
||||
|
||||
# X-Cache-Purge: bypass cache for this request (don't serve cached) but DO store the new response (overwrites that key)
|
||||
set $bypass_cache 0;
|
||||
if ($http_x_cache_purge != "") { set $bypass_cache 1; }
|
||||
proxy_cache_bypass $bypass_cache;
|
||||
proxy_no_cache 0;
|
||||
|
||||
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
|
||||
|
||||
add_header X-Cache-Status $upstream_cache_status always;
|
||||
}
|
||||
|
||||
location = /index.html {
|
||||
root /usr/share/nginx/html;
|
||||
# CORS headers for HTML interface
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
add_header Access-Control-Allow-Methods "GET, OPTIONS" always;
|
||||
add_header Access-Control-Allow-Headers "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type" always;
|
||||
|
||||
# Handle preflight OPTIONS requests
|
||||
if ($request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
test.sh
Normal file
14
test.sh
Normal file
@@ -0,0 +1,14 @@
|
||||
docker compose down
|
||||
|
||||
# Rebuild so current nginx.conf is copied into the image (no volume mount for config)
|
||||
docker compose up -d --build
|
||||
sleep 5
|
||||
|
||||
curl 'http://localhost:3000/?url=https://clickhouse-zkill.site.quack-lab.dev/?database=zkill&default_format=JSON' -H 'accept: */*' -H 'accept-language: en-US' -H 'authorization: Basic emtpbGxfcm9fdXNlcjp2Mk05WmdqWWtxeXB5UzRuZTlKdEs3QllwNjk0MnVqdQ==' -H 'content-type: text/plain; charset=utf-8' -H 'origin: http://localhost:8080' -H 'priority: u=1, i' -H 'referer: http://localhost:8080/' -H 'sec-ch-ua: "Not_A Brand";v="99", "Chromium";v="142"' -H 'sec-ch-ua-mobile: ?0' -H 'sec-ch-ua-platform: "Windows"' -H 'sec-fetch-dest: empty' -H 'sec-fetch-mode: cors' -H 'sec-fetch-site: cross-site' -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Cursor/2.4.21 Chrome/142.0.7444.235 Electron/39.2.7 Safari/537.36' --data-raw $'\n SELECT\n corporation_name,\n alliance_name,\n uniq(killmail_id) as kills_participated,\n uniq(character_name) as members_involved\n FROM zkill.killmail_attackers\n WHERE 1=1\n \n GROUP BY corporation_name, alliance_name\n ORDER BY kills_participated DESC\n '
|
||||
curl 'http://localhost:3000/?url=https://clickhouse-zkill.site.quack-lab.dev/?database=zkill&default_format=JSON' -H 'accept: */*' -H 'accept-language: en-US' -H 'authorization: Basic emtpbGxfcm9fdXNlcjp2Mk05WmdqWWtxeXB5UzRuZTlKdEs3QllwNjk0MnVqdQ==' -H 'content-type: text/plain; charset=utf-8' -H 'origin: http://localhost:8080' -H 'priority: u=1, i' -H 'referer: http://localhost:8080/' -H 'sec-ch-ua: "Not_A Brand";v="99", "Chromium";v="142"' -H 'sec-ch-ua-mobile: ?0' -H 'sec-ch-ua-platform: "Windows"' -H 'sec-fetch-dest: empty' -H 'sec-fetch-mode: cors' -H 'sec-fetch-site: cross-site' -H 'user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Cursor/2.4.21 Chrome/142.0.7444.235 Electron/39.2.7 Safari/537.36' --data-raw $'\n SELECT\n corporation_name,\n alliance_name,\n uniq(killmail_id) as kills_participated,\n uniq(character_name) as members_involved\n FROM zkill.killmail_attackers\n WHERE 1=1\n \n GROUP BY corporation_name, alliance_name\n ORDER BY kills_participated DESC\n '
|
||||
|
||||
sleep 3
|
||||
docker compose logs
|
||||
docker compose down
|
||||
|
||||
head result.json
|
||||
Reference in New Issue
Block a user