「CloudflareWorkers向けのCORS Proxy実装」の版間の差分
(ページの作成:「= Cloudflare Worker CORS Proxy = フロントエンドからのAPIリクエスト等で発生するCORSエラーを回避し、外部リソースを安全に取得する…」) |
(→特徴) |
||
| 6行目: | 6行目: | ||
* '''高速・省メモリ''' | * '''高速・省メモリ''' | ||
*: Cloudflare Workersのストリーム処理を活用し、データをバッファリングせずにそのまま転送します。 | *: Cloudflare Workersのストリーム処理を活用し、データをバッファリングせずにそのまま転送します。 | ||
| − | * ''' | + | * '''セキュリティ''' |
*: 事前に指定した許可ドメイン(Origin/Referer)からのアクセスのみを受け付けます。 | *: 事前に指定した許可ドメイン(Origin/Referer)からのアクセスのみを受け付けます。 | ||
*: 意図しないセッションや認証情報の漏洩を防ぐため、デフォルトで <code>Cookie</code> や <code>Authorization</code> ヘッダーを遮断します。 | *: 意図しないセッションや認証情報の漏洩を防ぐため、デフォルトで <code>Cookie</code> や <code>Authorization</code> ヘッダーを遮断します。 | ||
*: ターゲットサーバーにはプロキシ元の Origin/Referer を隠蔽し、ホットリンク対策によるアクセス拒否を回避します。 | *: ターゲットサーバーにはプロキシ元の Origin/Referer を隠蔽し、ホットリンク対策によるアクセス拒否を回避します。 | ||
| − | * ''' | + | * '''拡張性''' |
*: OPTIONSメソッド(プリフライト)に対応し、パラメータ指定で必要に応じたヘッダーのパススルーも可能です。 | *: OPTIONSメソッド(プリフライト)に対応し、パラメータ指定で必要に応じたヘッダーのパススルーも可能です。 | ||
2026年8月5日 (水) 05:20時点における最新版
Cloudflare Worker CORS Proxy
フロントエンドからのAPIリクエスト等で発生するCORSエラーを回避し、外部リソースを安全に取得するために使用する専用プロキシ(Cloudflare Workers用)です。
特徴
- 高速・省メモリ
- Cloudflare Workersのストリーム処理を活用し、データをバッファリングせずにそのまま転送します。
- セキュリティ
- 事前に指定した許可ドメイン(Origin/Referer)からのアクセスのみを受け付けます。
- 意図しないセッションや認証情報の漏洩を防ぐため、デフォルトで
CookieやAuthorizationヘッダーを遮断します。 - ターゲットサーバーにはプロキシ元の 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,
},
}
);
}
},
};