최신 업데이트:2024-09-03 17:00:40
이 예제에서는 Edge Cloud Apps 함수를 사용하여 전체 웹사이트에 대한 역방향 프록시를 구축하는 방법을 보여줍니다. 요청 URL, 헤더, 응답 헤더 및 응답 본문을 재작성하여 원활한 액세스를 달성합니다. 스트리밍 및 비스트리밍 재작성 방법을 모두 결합하여 웹사이트 콘텐츠를 유연하게 수정하여 다른 도메인으로 프록시하는 데 적합합니다. 이는 이전 웹사이트 콘텐츠를 새 도메인으로 마이그레이션하거나 타사 웹사이트 콘텐츠를 통합하는 등의 시나리오에 유용합니다.
이 예제에서는 www.gutenberg.org
의 콘텐츠가 your-domain.com/gutenberg-proxy
경로로 프록시됩니다. 프록시된 웹사이트가 제대로 작동하도록 요청과 응답 모두에 재작성이 적용됩니다.
다음과 같은 수정이 이루어집니다.
Host
, Referer
및 Origin
필드가 오리진을 가리키도록 수정됩니다.Set-Cookie
및 Location
필드가 프록시 도메인과 일치하도록 수정됩니다.text/html
: HTMLRewriter
를 사용하여 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));
});