このサーバーは以下を実装しています:
・TLS はリバースプロキシ(nginx)で終端する想定
・入力 URL の検証とホワイトリスト(推奨)
・ヘッダ除去(Referer, Cookies, Authorization など)
・CSP ヘッダ付与、レスポンス型に応じた処理、基本認証、レート制限、簡易キャッシュ、ログ最小化
・Dockerfile と systemd / nginx サンプル、Let's Encrypt 手順を付属
// -- server.js (production-ready example)
// npm i express node-fetch helmet express-rate-limit lru-cache basic-auth
const express = require('express');
const fetch = require('node-fetch');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const LRU = require('lru-cache');
const basicAuth = require('basic-auth');
const { URL } = require('url');
const app = express();
// ---------- Configuration (tune for your environment) ----------
const PORT = process.env.PORT || 3000;
const ALLOWLIST = process.env.ALLOWLIST ? process.env.ALLOWLIST.split(',') : []; // optional host allowlist (e.g. example.com,example.org)
const REQUIRE_BASIC_AUTH = process.env.REQUIRE_BASIC_AUTH === '1'; // set to '1' to enable simple basic auth
const BASIC_AUTH_USER = process.env.BASIC_AUTH_USER || 'proxy';
const BASIC_AUTH_PASS = process.env.BASIC_AUTH_PASS || 'change_this';
const MAX_BODY = 5 * 1024 * 1024; // 5MB max
const CACHE_MAX = 100;
const CACHE_TTL_MS = 60 * 1000; // 1 minute simple cache
// ---------------------------------------------------------------
// Security headers
app.use(helmet({
contentSecurityPolicy: false // We'll set a controlled CSP per-response
}));
// Rate limiter — protect from abuse
const limiter = rateLimit({
windowMs: 60*1000, // 1 minute
max: 120, // requests per IP per window
standardHeaders: true,
legacyHeaders: false
});
app.use(limiter);
// Simple in-memory cache
const cache = new LRU({ max: CACHE_MAX, ttl: CACHE_TTL_MS });
// Basic auth middleware (optional)
function requireBasicAuth(req, res, next){
if (!REQUIRE_BASIC_AUTH) return next();
const user = basicAuth(req);
if (!user || user.name !== BASIC_AUTH_USER || user.pass !== BASIC_AUTH_PASS){
res.set('WWW-Authenticate', 'Basic realm="Protected Proxy"');
return res.status(401).send('Authentication required');
}
next();
}
// Health check
app.get('/healthz', (req, res) => res.send('ok'));
// Main fetch endpoint
app.get('/fetch', requireBasicAuth, async (req, res) => {
const target = req.query.url;
if (!target) return res.status(400).send('missing url');
// Validate URL
let parsed;
try { parsed = new URL(target); }
catch (e) { return res.status(400).send('invalid url'); }
// Optional allowlist check
if (ALLOWLIST.length > 0){
const host = parsed.hostname;
if (!ALLOWLIST.includes(host)) return res.status(403).send('host not allowed');
}
// Only allow http/https
if (!/^https?:$/.test(parsed.protocol)) return res.status(400).send('unsupported protocol');
// Prevent local network / link-local access (basic check)
const bannedHosts = ['localhost','127.0.0.1','::1'];
if (bannedHosts.includes(parsed.hostname)) return res.status(403).send('access denied');
// Build fetch options — strip incoming headers that shouldn't be forwarded
const fetchOptions = {
method: 'GET',
redirect: 'follow',
compress: true,
headers: {
'User-Agent': 'PrivacyProxy/1.0 (+https://yourdomain.example/)',
'Accept': '*/*'
// Do NOT forward client cookies, authorization, referer, etc.
}
};
const cacheKey = target;
if (cache.has(cacheKey)) {
const cached = cache.get(cacheKey);
res.set(cached.headers);
return res.status(200).send(cached.body);
}
try {
const upstream = await fetch(target, fetchOptions);
const contentType = upstream.headers.get('content-type') || '';
// Read body (guard size)
const buf = await upstream.buffer();
if (buf.length > MAX_BODY) return res.status(413).send('resource too large');
// Build safe response headers
const safeHeaders = {
'Content-Security-Policy': "default-src 'none'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self' data:; connect-src 'none'; frame-ancestors 'none';",
'Referrer-Policy': 'no-referrer',
'X-Content-Type-Options': 'nosniff',
'X-Frame-Options': 'DENY',
'Cache-Control': 'private, max-age=60'
};
// If HTML, we may inject a CSP meta etc — but we also attach above headers.
// Proxy should not forward Set-Cookie or Authorization
// Return content-type from upstream when reasonable
if (contentType) safeHeaders['Content-Type'] = contentType;
// Minimal logging — DO NOT log full content or sensitive headers
console.log(`[proxy] ${req.ip} => ${target} [${upstream.status}] size=${buf.length}`);
// Cache HTML/text responses (only small ones)
if (buf.length < 1024*200) { // cache small responses
cache.set(cacheKey, { headers: safeHeaders, body: buf });
}
// Send
Object.entries(safeHeaders).forEach(([k,v]) => res.set(k, v));
return res.status(200).send(buf);
} catch (err) {
console.error('fetch error', err && err.message);
return res.status(502).send('upstream fetch error');
}
});
// Simple root info
app.get('/', (req, res) => res.send('Privacy Proxy — minimal server. Use /fetch?url=...'));
// Start
app.listen(PORT, () => console.log(`Privacy proxy listening on ${PORT}`));
(環境変数で ALLOWLIST, REQUIRE_BASIC_AUTH, BASIC_AUTH_* を設定してください)