Site Proxy

最終更新日:2024-09-03 17:00:40

この例では、Edge Cloud Apps関数を使用して、Webサイト全体のリバースプロキシを構築する方法を示します。リクエストURL、ヘッダー、レスポンスヘッダー、およびレスポンス本文を書き換えることにより、シームレスなアクセスが実現します。ストリーミングと非ストリーミングの両方の書き換え方法を組み合わせているため、Webサイトのコンテンツを柔軟に変更して別のドメインにプロキシするのに適しています。これは、古いWebサイトのコンテンツを新しいドメインに移行したり、サードパーティのWebサイトのコンテンツを統合したりするなどのシナリオに役立ちます。

この例では、www.gutenberg.orgのコンテンツがyour-domain.com/gutenberg-proxyパスにプロキシされます。プロキシされたWebサイトが正しく機能するように、リクエストとレスポンスの両方に書き換えが適用されます。

次の変更が行われます。

  • **リクエストURLの書き換え:**ユーザーがアクセスしたプロキシURLが、オリジンURLに変換されます。
  • **リクエストヘッダーの書き換え:**リクエストヘッダーのHostReferer、およびOriginフィールドが、オリジンを指すように変更されます。
  • **レスポンスヘッダーの書き換え:**レスポンスヘッダーのSet-CookieおよびLocationフィールドが、プロキシドメインと一致するように変更されます。
  • **レスポンス本文の書き換え:**レスポンスのコンテンツタイプに基づいて、異なる書き換え操作が実行されます。
    • text/htmlHTMLRewriterを使用して、HTMLタグ内のURLを書き換えます。
    • text/css:正規表現を使用して、CSSファイル内のurl()関数を書き換えます。
    • application/x-javascriptおよびapplication/json:正規表現を使用して、JavaScriptおよびJSONファイル内のURLを書き換えます。

コード例

const UPSTREAM = "www.gutenberg.org"; // オリジンドメイン
const PATH_PREFIX = "/gutenberg-proxy"; // プロキシパスプレフィックス
const RW_START = "/--rw--"; // プロキシドメインの開始識別子
const RW_STOP = "--wr--"; // プロキシドメインの終了識別子
let g_origin_url; // 元のリクエストURLを保存します
let g_path_pfrefix; // プロキシパスプレフィックスを保存します

async function handleRequest(request) {
    g_origin_url = new URL(request.url); 

    // POSTリクエストを処理します
    if (request.method === "POST") {
        let body = await request.text();
        // 必要に応じてPOST本文を処理します
    }
    
    // URLを復元して、オリジンURLとプロキシパスプレフィックスを取得します
    const {urlString, pathPrefix} = restoreURL(request.url); 
    g_path_pfrefix = pathPrefix;

    const url = new URL(urlString);
    console.log("request.url", request.url);
    console.log("復元されたURL", urlString); 

    // 特別な処理:ungzipエラーを回避するために、Accept-Encodingヘッダーを削除します
    if (url.pathname === "/browse/scores/top") {
        request.headers.delete("Accept-Encoding");
    }

    // 新しいリクエストを作成し、リクエストヘッダーを変更します
    const newRequest = new Request(urlString, request);
    const headers = newRequest.headers;
    headers.set("Host", url.host); // Hostヘッダーをオリジンドメインに設定します
    
    // Refererヘッダーを書き換えます
    const referer = headers.get("Referer");
    if (referer) {
        const {urlString, pathPrefix} = restoreURL(referer);
        headers.set("Referer", urlString); 
        if (!url.pathname.endsWith(".css")) {
            g_path_pfrefix = pathPrefix;
        }
    }

    // Originヘッダーを書き換えます
    const origin = headers.get("Origin");
    if (origin) {
        const {urlString} = restoreURL(origin);
        headers.set("Origin", urlString);
    }

    // オリジンにリクエストを送信します
    const response = await fetch(newRequest, {cdnProxy: false, redirect: "manual"}); 
    const responseHeaders = response.headers;

    // Set-Cookieヘッダーを書き換えます
    let cookie = responseHeaders.get("Set-Cookie");
    if (cookie) {
        cookie = cookie.replace(/(domain=)([^;]+);/gi, '$1'+g_origin_url.host+';');
        responseHeaders.set("Set-Cookie", cookie);
    }

    // Locationヘッダーを書き換えます
    let location = responseHeaders.get("Location");
    if (location) {
        location = rewriteURL(location);
        responseHeaders.set("Location", location);
    }

    const contentType = getResponseHeader(response, "Content-Type");
    
    // レスポンスのコンテンツタイプに基づいて、異なる書き換え操作を実行します
    if (contentType.includes("text/html")) {
        return new HTMLRewriter()
                    .on("a", new URLHandler(["href", "data-url", "data-verify-url"]))
                    .on("link", new URLHandler(["href"]))
                    .on("script", new URLHandler(["src"]))
                    .on("iframe", new URLHandler(["src"]))
                    .on("input", new URLHandler(["src"]))
                    .on("div", new URLHandler(["style", "data-url", "data-status-url"]))
                    .on("img", new URLHandler(["src", "data-origin"]))
                    .on("form", new URLHandler(["action"]))
                    .on("meta", new URLHandler(["content"]))
                    .on("span", new URLHandler(["data-verify-url"]))
                    .transform(response);
    } else if (contentType.includes("text/css")) {
        let text = await response.text();
        text = rewriteText(text, /url\((.*?)\)/g);
        return new Response(text, response);
    } else if (contentType.includes("application/x-javascript")) {
        let text = await response.text();
        text = text.replace(/https:\\\/\\\//g, "https://");
        text = rewriteText(text, /'(\/j\/subject\/)'/g);
        text = rewriteText(text, /"https?:(\/\/.*?)"/gi);
        text = rewriteText(text, /'https?:(\/\/.*?)'/gi);
        text = rewriteText(text, /\.get\("(.*?)\"/g);
        return new Response(text, response);
    } else if (contentType.includes("application/json")) {
        let text = await response.text();
        text = rewriteText(text, /"https?:(\/\/.*?)"/gi);
        return new Response(text, response);
    } else {
        return response;
    }
}

// レスポンスヘッダーを取得します
function getResponseHeader(response, headerName) {
    const value = response.headers.get(headerName);
    return value ? value.toLowerCase() : "";
}

// テキスト内のURLを書き換えます
function rewriteText(text, reg) {
    let result = text.replace(reg,  function(match, str){
        let result = match.replace(str, rewriteURL(str));
        result = result.replace("https", "http");
        return result;
    });
    return result;
}

// HTML要素内のURLを処理します
class URLHandler {
    constructor(attrs) {
        this.attrs = attrs;
    }
    text(text) {
        let result = rewriteText(text.text, /':?(\/\/.*?)'/g);
        result = rewriteText(result, /"https?:(\/\/.*?)"/gi);
        result = rewriteText(result, /'https?:(\/\/.*?)'/gi);
        if (result != text.text) {
            text.replace(result);
        }
        
    }
	element(element) {
        for (let attr of this.attrs) {
            const href1 = element.getAttribute(attr);
            if (!href1) continue;
            let href2;
            if (attr === "style") {
                href2 = rewriteText(href1,  /url\((.*?)\)/g);
            } else {
                href2 = rewriteURL(href1);
            }
            if (href1 != href2) {
                element.setAttribute(attr, href2);
            }
        }
	}
}

// URLを書き換えて、オリジンURLをプロキシURLに変換します
function rewriteURL(originURL) {
    if (!originURL.startsWith("/") && !originURL.startsWith("http")) {
        return originURL;
    }
    originURL = originURL.replace(///g, "/").replace(/\\\//g, "/");
    if (originURL.startsWith("https://")) {
        originURL = originURL.replace("https://", "http://");
    }
    let fullURL = originURL;
    if (originURL.startsWith("//")) {
        fullURL = "http:" + originURL;
    } else if (originURL.startsWith("/")) {
        return g_path_pfrefix + originURL;
    }
    try {
        const url = new URL(fullURL);
        let host = '';
        if (url.host != UPSTREAM) {
            host = `${RW_START}${url.host.replace(/\./g, "---")}${RW_STOP}`;
        }
        const rw = `${g_origin_url.host}${PATH_PREFIX}${host}`;
        return originURL.replace(url.host, rw); 
    } catch (e) {
        console.error("リライターエラー", e, originURL);
        return originURL;
    }
}

// URLを復元して、プロキシURLをオリジンURLに戻します
function restoreURL(rewritedURL) {
    if (rewritedURL.endsWith(PATH_PREFIX) || rewritedURL.endsWith(RW_STOP)) {
        rewritedURL += "/";
    }

    const url = new URL(rewritedURL);
    let pathname = url.pathname;
    let pathPrefix, host;

    if (pathname.startsWith(PATH_PREFIX)) {
        pathname = pathname.substring(PATH_PREFIX.length);
    }
    
    if (pathname.startsWith(RW_START) && pathname.includes(RW_STOP)) {
        const stop = pathname.indexOf(RW_STOP);
        pathPrefix = PATH_PREFIX + pathname.substring(0, stop + RW_STOP.length);
        host = pathname.substring(RW_START.length, stop).replace(/---/g, ".");
    } else {
        host = UPSTREAM;
        pathPrefix = PATH_PREFIX;
    }

    return {
        urlString: rewritedURL.replace(url.protocol, "https:").replace(url.host, host).replace(pathPrefix, ''), 
        pathPrefix: pathPrefix
    };
}
                    
addEventListener("fetch", event => {
    return event.respondWith(handleRequest(event.request));
});