CloudflareWorkers向けのCORS Proxy実装

提供: svg2wiki
2026年8月5日 (水) 05:19時点におけるAdmin (トーク | 投稿記録)による版 (ページの作成:「= Cloudflare Worker CORS Proxy = フロントエンドからのAPIリクエスト等で発生するCORSエラーを回避し、外部リソースを安全に取得する…」)
(差分) ← 古い版 | 最新版 (差分) | 新しい版 → (差分)
移動先: 案内検索

Cloudflare Worker CORS Proxy

フロントエンドからのAPIリクエスト等で発生するCORSエラーを回避し、外部リソースを安全に取得するために使用する専用プロキシ(Cloudflare Workers用)です。

特徴

  • 高速・省メモリ
    Cloudflare Workersのストリーム処理を活用し、データをバッファリングせずにそのまま転送します。
  • 強固なセキュリティ
    事前に指定した許可ドメイン(Origin/Referer)からのアクセスのみを受け付けます。
    意図しないセッションや認証情報の漏洩を防ぐため、デフォルトで CookieAuthorization ヘッダーを遮断します。
    ターゲットサーバーにはプロキシ元の Origin/Referer を隠蔽し、ホットリンク対策によるアクセス拒否を回避します。
  • 柔軟な拡張性
    OPTIONSメソッド(プリフライト)に対応し、パラメータ指定で必要に応じたヘッダーのパススルーも可能です。

使い方

プロキシURLの file パラメータに、取得したいターゲットURLをURLエンコードして渡します。 ※ 欠落を防ぐため、JavaScriptでは必ず encodeURIComponent を使用してください(encodeURI は不可)。

基本的なリクエスト

デフォルトではセキュリティを考慮し、認証情報やCookieを含めずにプロキシします。

<syntaxhighlight lang="javascript"> const targetUrl = "https://example.com/api/data"; const proxyUrl = `https://your-worker.workers.dev/?file=${encodeURIComponent(targetUrl)}`;

fetch(proxyUrl)

 .then(response => response.json())
 .then(data => console.log(data))
 .catch(error => console.error(error));

</syntaxhighlight>

オプション付きリクエスト

以下のクエリパラメータを付与することで、ヘッダー情報の転送を許可できます。

  • pass_auth=1 : クライアントからの Authorization ヘッダー転送を許可する
  • pass_cookie=1 : クライアントからの Cookie、およびターゲットからの Set-Cookie 転送を許可する

例:フロントエンドから認証ヘッダーを送ってAPIを叩く場合 <syntaxhighlight lang="javascript"> const proxyUrl = `https://your-worker.workers.dev/?file=${encodeURIComponent(targetUrl)}&pass_auth=1`; </syntaxhighlight>

ソースコード

以下のコードを Cloudflare Workers のスクリプトとしてデプロイして使用します。

/**
 * ==============================================================================
 * Cloudflare Worker CORS Proxy (Edge-Optimized & Secure)
 * ==============================================================================
 *
 * 【目的】
 * 指定した許可ドメイン(自サイト等)からのみアクセス可能な専用のCORSプロキシ。
 * フロントエンドのCORSエラーを回避し、外部リソースやAPIを安全に取得するために使用する。
 *
 * 【狙い・特徴】
 * 1. 高速・省メモリ: 
 * Cloudflare Workersのストリーム処理を活かし、データをバッファリングせずそのまま転送。
 * 2. 強固なセキュリティ: 
 * - 許可された Origin/Referer からのアクセスのみを受け付ける。
 * - 意図しないセッションや認証情報の漏洩を防ぐため、デフォルトで Cookie / Authorization を遮断。
 * - ターゲットへはプロキシ元の Origin/Referer を隠蔽し、ホットリンク対策等の弾きを回避。
 * 3. 柔軟な拡張性: 
 * OPTIONSメソッド(プリフライト)の動的許可や、必要に応じたヘッダーのパススルーに対応。
 *
 * 【使い方】
 * プロキシURLの `file` パラメータに、取得したいターゲットURLをエンコードして渡す。
 * ※ 欠落を防ぐため、必ず `encodeURIComponent` を使用すること(`encodeURI` は不可)。
 *
 * [基本リクエスト(デフォルト・セキュア)]
 * const targetUrl = "https://example.com/api/data";
 * const proxyUrl = `https://your-worker.workers.dev/?file=${encodeURIComponent(targetUrl)}`;
 * fetch(proxyUrl) // ...
 *
 * [オプション(クエリパラメータ)]
 * - pass_auth=1   : クライアントからの `Authorization` ヘッダー転送を許可する
 * - pass_cookie=1 : クライアントからの `Cookie`、およびターゲットからの `Set-Cookie` 転送を許可する
 * * 例(フロントエンドから認証ヘッダーを送ってAPIを叩く場合):
 * `https://.../?file=${encodeURIComponent(targetUrl)}&pass_auth=1`
 * ==============================================================================
 */
 
export default {
	async fetch(request, env, ctx) {
		// ========================================================
		// 1. プロキシ対象URLとオプションの取得
		// ========================================================
		const url = new URL(request.url);
		const fileUrl = url.searchParams.get("file");

		// オプション("1" の場合はヘッダーのパススルーを許可)
		const passAuth = url.searchParams.get("pass_auth") === "1";
		const passCookie = url.searchParams.get("pass_cookie") === "1";

		if (!fileUrl) {
			return new Response(
				JSON.stringify({ error: "Missing 'file' parameter with target URL." }),
				{
					status: 400,
					headers: { "Content-Type": "application/json; charset=utf-8" },
				}
			);
		}

		// ========================================================
		// 2. アクセス元(Origin/Referer)フィルター
		// ========================================================
		const allowedOrigins = [
			"svgmap.org",
			"sssvgmap.stars.ne.jp",
			"svgmap.github.io",
		];

		const origin = request.headers.get("Origin");
		const referer = request.headers.get("Referer");
		const clientUrl = origin || referer || "";

		let isAllowed = false;
		let allowOriginHeader = "";

		if (clientUrl) {
			try {
				const urlObj = new URL(clientUrl);
				const hostname = urlObj.hostname;

				for (const domain of allowedOrigins) {
					if (hostname === domain || hostname.endsWith("." + domain)) {
						isAllowed = true;
						allowOriginHeader = origin || `${urlObj.protocol}//${hostname}`;
						break;
					}
				}
			} catch (e) {
				// URLのパース失敗時は弾く
			}
		}

		if (!isAllowed) {
			return new Response(
				JSON.stringify({ error: "Access Denied: Invalid Origin/Referer" }),
				{
					status: 403,
					headers: { "Content-Type": "application/json; charset=utf-8" },
				}
			);
		}

		// ========================================================
		// 3. CORS 共通処理 & プリフライト(OPTIONS)応答
		// ========================================================
		// クライアントがカスタムヘッダーを送りたい場合に備え、リクエストされたヘッダーを動的に許可
		const requestedHeaders = request.headers.get(
			"Access-Control-Request-Headers"
		);
		const allowedHeaders = requestedHeaders || "Content-Type, Authorization";

		const corsHeaders = {
			"Access-Control-Allow-Origin": allowOriginHeader,
			"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
			"Access-Control-Allow-Headers": allowedHeaders,
		};

		if (request.method === "OPTIONS") {
			return new Response(null, { headers: corsHeaders });
		}

		// ========================================================
		// 4. プロキシとしての Fetch 処理(ヘッダー検疫 & ストリーム)
		// ========================================================
		const controller = new AbortController();
		const timeoutId = setTimeout(() => controller.abort(), 30000); // 30秒タイムアウト

		try {
			const fetchHeaders = new Headers(request.headers);

			// ターゲットにプロキシ元の情報を漏らさない(アクセス拒否対策)
			fetchHeaders.delete("Origin");
			fetchHeaders.delete("Referer");

			// セキュリティ: Cookieの意図しない流出を防ぐ(オプションで許可可能)
			if (!passCookie) {
				fetchHeaders.delete("Cookie");
				fetchHeaders.delete("Cookie2");
			}

			// セキュリティ: 意図しない認証情報の流出を防ぐ(オプションで許可可能)
			if (!passAuth) {
				fetchHeaders.delete("Authorization");
			}

			// ユーザーエージェントを一般的なブラウザに偽装
			fetchHeaders.set(
				"User-Agent",
				"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
			);

			const fetchOptions = {
				method: request.method,
				headers: fetchHeaders,
				redirect: "follow",
				signal: controller.signal,
			};

			// POST等の場合、Bodyをストリームとしてそのまま転送
			if (request.method !== "GET" && request.method !== "HEAD") {
				fetchOptions.body = request.body;
			}

			// ターゲットURLへリクエスト
			const response = await fetch(fileUrl, fetchOptions);
			clearTimeout(timeoutId);

			// ========================================================
			// 5. レスポンスヘッダーの構築と返却
			// ========================================================
			const responseHeaders = new Headers(response.headers);

			// ターゲットからの Cookie をクライアントに書き込ませない(オプションで許可可能)
			if (!passCookie) {
				responseHeaders.delete("Set-Cookie");
				responseHeaders.delete("Set-Cookie2");
			}

			// 事前に生成した CORS 許可ヘッダーを付与
			responseHeaders.set("Access-Control-Allow-Origin", allowOriginHeader);

			// フロントエンドのJSが読み取れるように有用なヘッダーを公開
			const exposedHeaders = [
				"Content-Length",
				"Content-Type",
				"Content-Disposition",
				"Last-Modified",
			].join(", ");
			responseHeaders.set("Access-Control-Expose-Headers", exposedHeaders);

			// Content-Typeのフォールバック
			if (!responseHeaders.has("Content-Type")) {
				responseHeaders.set("Content-Type", "application/octet-stream");
			}

			// ターゲットからのHTTPステータスとボディ(ストリーム)をそのまま返す
			return new Response(response.body, {
				status: response.status,
				statusText: response.statusText,
				headers: responseHeaders,
			});
		} catch (error) {
			clearTimeout(timeoutId);

			if (error.name === "AbortError") {
				return new Response(JSON.stringify({ error: "Request timed out." }), {
					status: 504,
					headers: {
						"Content-Type": "application/json; charset=utf-8",
						...corsHeaders,
					},
				});
			}

			return new Response(
				JSON.stringify({ error: `Internal Server Error: ${error.message}` }),
				{
					status: 500,
					headers: {
						"Content-Type": "application/json; charset=utf-8",
						...corsHeaders,
					},
				}
			);
		}
	},
};