Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,16 +30,22 @@ npx @humanwhocodes/proxy-fetch-server

The server is configured using environment variables:

- **PROXY_URI** (required) - The address of the proxy to use with the proxy agent
- **http_proxy** (conditionally required) - The proxy server to use for requests that use the http protocol
- **https_proxy** (conditionally required) - The proxy server to use for requests that use the https protocol
- **no_proxy** (optional) - A comma-delimited list of hostnames or hostname:port entries that should bypass using the configured proxy completely. If a hostname begins with a dot (.) then it applies to all subdomains. For instance `.humanwhocodes.com` applies to `humanwhocodes.com`, `www.humanwhocodes.com`, `newsletter.humanwhocodes.com`, etc.
- **PROXY_FETCH_KEY** (optional) - The expected Bearer token in the Authorization header
- **PORT** (optional) - The port to start the server on (default: 8080)
- **PROXY_TOKEN** (optional) - The token that the proxy expects
- **PROXY_TOKEN_TYPE** (optional) - The token type prefix for the proxy (default: "Bearer")

Either `http_proxy` or `https_proxy` is required.

Example:

```shell
PROXY_URI=http://proxy.example.com:8080 \
http_proxy=http://proxy.example.com:8080 \
https_proxy=http://proxy.example.com:8080 \
no_proxy=localhost,.internal.com \
PROXY_FETCH_KEY=my-secret-key \
PORT=3000 \
PROXY_TOKEN=proxy-secret \
Expand All @@ -64,6 +70,7 @@ curl -X POST http://localhost:8080/ \
```

The server will:

1. Validate the Bearer token (if configured)
2. Fetch the specified URL through the configured proxy
3. Return the response with the same status code and content type
Expand All @@ -77,14 +84,27 @@ import { createApp } from "@humanwhocodes/proxy-fetch-server";

const app = createApp({
key: "my-secret-key",
proxyUri: "http://proxy.example.com:8080",
httpProxy: "http://proxy.example.com:8080",
httpsProxy: "http://proxy.example.com:8080",
noProxy: ["localhost", ".internal.com"],
proxyToken: "proxy-secret",
proxyTokenType: "Bearer",
});

// Use with your preferred Node.js server adapter
```

**Configuration options:**

- `key` (string, optional) - The expected Bearer token in the Authorization header
- `httpProxy` (string, conditionally required) - The proxy server to use for HTTP requests
- `httpsProxy` (string, conditionally required) - The proxy server to use for HTTPS requests
- `noProxy` (string[], optional) - Array of hostnames or hostname:port entries to bypass proxy
- `proxyToken` (string, optional) - The token that the proxy expects
- `proxyTokenType` (string, optional) - The token type prefix for the proxy (default: "Bearer")

Either `httpProxy` or `httpsProxy` is required.

## License

Copyright 2025 Nicholas C. Zakas
Expand Down
110 changes: 88 additions & 22 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,30 @@ import { ProxyAgent } from "undici";
* Creates the Hono app with the given configuration
* @param {object} config - Configuration options
* @param {string} [config.key] - Expected Bearer token (optional)
* @param {string} config.proxyUri - Proxy URI (required)
* @param {string} [config.httpProxy] - HTTP proxy URI
* @param {string} [config.httpsProxy] - HTTPS proxy URI
* @param {string[]} [config.noProxy] - Array of hostnames or hostname:port to bypass proxy
* @param {string} [config.proxyToken] - Proxy token
* @param {string} [config.proxyTokenType] - Proxy token type prefix (default: "Bearer")
* @returns {Hono} The configured Hono app
*/
function createApp(config) {
const app = new Hono();

const { key, proxyUri, proxyToken, proxyTokenType = "Bearer" } = config;
const {
key,
httpProxy,
httpsProxy,
noProxy = [],
proxyToken,
proxyTokenType = "Bearer",
} = config;

// Validate required configuration
if (!proxyUri) {
throw new Error("proxyUri is required in configuration");
if (!httpProxy && !httpsProxy) {
throw new Error(
"Either httpProxy or httpsProxy is required in configuration",
);
}

// Apply bearer auth middleware if key is provided
Expand All @@ -34,10 +45,45 @@ function createApp(config) {
}

/**
* POST / endpoint - Fetches a URL using a proxy agent
* Checks if a hostname should bypass the proxy
* @param {string} hostname - The hostname to check
* @param {string} port - The port (optional)
* @param {string[]} noProxyList - Array of hostnames or hostname:port to bypass
* @returns {boolean} True if should bypass proxy
*/
app.post("/", async (c) => {
function shouldBypassProxy(hostname, port, noProxyList) {
for (const entry of noProxyList) {
// Check for subdomain pattern (starts with .)
if (entry.startsWith(".")) {
const domain = entry.slice(1); // Remove leading dot

if (hostname === domain || hostname.endsWith(`.${domain}`)) {
return true;
}

continue;
}

// Check for hostname:port match
if (entry.includes(":")) {
const hostnamePort = port ? `${hostname}:${port}` : hostname;

if (entry === hostnamePort) {
return true;
}
} else if (entry === hostname) {
// Exact hostname match
return true;
}
}

return false;
}

/**
* POST / endpoint - Fetches a URL using a proxy agent
*/
app.post("/", async c => {
// Parse request body
/** @type {any} */
let body;
Expand Down Expand Up @@ -72,23 +118,42 @@ function createApp(config) {
);
}

// Create proxy agent with undici
/** @type {import('undici').ProxyAgent.Options} */
const proxyAgentOptions = {
uri: proxyUri,
};

// Add proxy token if configured
if (proxyToken) {
proxyAgentOptions.token = `${proxyTokenType} ${proxyToken}`;
}

const proxyAgent = new ProxyAgent(proxyAgentOptions);
// Check if we should bypass the proxy
const hostname = targetUrl.hostname;
const port = targetUrl.port;
const useProxy = !shouldBypassProxy(hostname, port, noProxy);

/** @type {Record<string, any>} */
const fetchOptions = {
dispatcher: proxyAgent,
};
const fetchOptions = {};

if (useProxy) {
// Determine which proxy to use based on the protocol
const selectedProxy =
targetUrl.protocol === "https:" ? httpsProxy : httpProxy;

// If the selected proxy is not configured, fall back to the other one
// At least one of httpProxy or httpsProxy is guaranteed to be defined
const proxyUri = selectedProxy || httpsProxy || httpProxy;

if (!proxyUri) {
// This should never happen due to validation, but satisfy TypeScript
throw new Error("No proxy URI available");
}

// Create proxy agent with undici
/** @type {import('undici').ProxyAgent.Options} */
const proxyAgentOptions = {
uri: proxyUri,
};

// Add proxy token if configured
if (proxyToken) {
proxyAgentOptions.token = `${proxyTokenType} ${proxyToken}`;
}

const proxyAgent = new ProxyAgent(proxyAgentOptions);
fetchOptions.dispatcher = proxyAgent;
}

// Fetch the URL
try {
Expand All @@ -97,7 +162,8 @@ function createApp(config) {
// Pass through the response using arrayBuffer for proper binary handling
const responseBody = await response.arrayBuffer();
const contentType =
response.headers.get("Content-Type") || "application/octet-stream";
response.headers.get("Content-Type") ||
"application/octet-stream";

return new Response(responseBody, {
status: response.status,
Expand Down
23 changes: 18 additions & 5 deletions src/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,36 @@ import { createApp } from "./app.js";
// Get configuration from environment variables
const key = process.env.PROXY_FETCH_KEY;
const port = parseInt(process.env.PORT || "8080", 10);
const proxyUri = process.env.PROXY_URI;
const httpProxy = process.env.http_proxy;
const httpsProxy = process.env.https_proxy;
const noProxyEnv = process.env.no_proxy;
const proxyToken = process.env.PROXY_TOKEN;
const proxyTokenType = process.env.PROXY_TOKEN_TYPE;

// Parse no_proxy into an array
const noProxy = noProxyEnv
? noProxyEnv.split(",").map(entry => entry.trim())
: [];

// Validate required configuration
if (!proxyUri) {
if (!httpProxy && !httpsProxy) {
console.error(
"Error: PROXY_URI environment variable is required",
"Error: Either http_proxy or https_proxy environment variable is required",
);
process.exit(1);
}

const app = createApp({ key, proxyUri, proxyToken, proxyTokenType });
const app = createApp({
key,
httpProxy,
httpsProxy,
noProxy,
proxyToken,
proxyTokenType,
});

console.log(`Starting server on port ${port}...`);
serve({
fetch: app.fetch,
port,
});

Loading