You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Cloudflare反向代理如何重写加载后JS注入的动态链接

问题场景

参考公开的CORSflare配置教程在Cloudflare部署了作为反向代理的Service Worker,用于将非预定义网页嵌入iFrame。页面初始加载阶段代理逻辑运行正常,但无法处理页面加载完成后JavaScript动态注入的链接,这类链接会直接基于代理服务的根路径加载,跳转行为不符合预期:

/path -> myproxy.cloudflare.com/path [错误行为]
/path -> myproxy.cloudflare.com?link=query_domain.com/path [预期行为]

需要实现动态生成URL的重写逻辑,匹配预设的代理路由规则。

实现方案

静态HTML替换只能处理服务端返回的原始内容,无法拦截页面运行时JS生成的DOM、动态请求和跳转,需要从三个层面补全拦截逻辑,三层逻辑互相兜底:

  • 第一层:页面加载最早期注入JS垫片,劫持所有URL相关原生API
    在返回HTML响应时,把自执行JS垫片插入到<head>标签的第一个子节点位置,保证垫片在所有业务JS之前运行,从API层面拦截所有URL生成、跳转、请求行为:

    • 劫持document.createElement方法,针对a/iframe/img/script/link/form这类携带href/src/actionURL属性的元素,重写对应属性的setter,所有赋值给这些属性的地址都会先经过代理规则转换,再赋值给原生属性。
    • 劫持所有跳转相关API:包括window.location的assign/replace方法、直接给location.href/location.pathname赋值的行为、history.pushState/history.replaceState路由方法,所有触发页面跳转、路由更新的逻辑都先做URL转换再执行原生逻辑。
    • 劫持所有动态请求API:重写全局fetch、XMLHttpRequest.prototype.open方法,所有异步请求的地址也走同样的代理转换规则,避免接口、动态资源请求直接打到代理域名根路径。

    垫片核心代码参考:

    (function() {
      // URL转代理格式的核心逻辑,根据自身业务规则调整判断条件
      const proxify = (rawUrl) => {
        if (!rawUrl) return rawUrl;
        // 跳过特殊协议、已经是代理格式的地址
        if (rawUrl.startsWith('?link=') || /^(javascript|mailto|tel):/i.test(rawUrl)) return rawUrl;
        try {
          const urlObj = new URL(rawUrl, window.location.href);
          // 代理域下非代理路径的地址,统一转成link参数格式
          if (urlObj.origin === window.location.origin && !urlObj.searchParams.get('link')) {
            return `?link=${encodeURIComponent(urlObj.pathname + urlObj.search + urlObj.hash)}`;
          }
          // 如果是固定代理单个站点,这里补充把目标站点的绝对路径也转成代理格式即可
          return rawUrl;
        } catch(e) {
          return rawUrl;
        }
      }
    
      // 劫持元素创建逻辑
      const originCreateElement = document.createElement.bind(document);
      document.createElement = function(tagName) {
        const el = originCreateElement(tagName);
        const tag = tagName.toLowerCase();
        let urlAttrs = [];
        if (['a','area'].includes(tag)) urlAttrs = ['href'];
        if (['iframe','img','script','embed','audio','video','source'].includes(tag)) urlAttrs = ['src'];
        if (tag === 'link') urlAttrs = ['href'];
        if (tag === 'form') urlAttrs = ['action'];
    
        urlAttrs.forEach(attr => {
          const originDesc = Object.getOwnPropertyDescriptor(el.__proto__, attr) || Object.getOwnPropertyDescriptor(HTMLElement.prototype, attr);
          if (!originDesc) return;
          Object.defineProperty(el, attr, {
            get: () => originDesc.get.call(el),
            set: (val) => originDesc.set.call(el, proxify(val))
          });
        });
        return el;
      }
    
      // 劫持跳转API
      const originAssign = window.location.assign.bind(window.location);
      window.location.assign = (url) => originAssign(proxify(url));
      const originReplace = window.location.replace.bind(window.location);
      window.location.replace = (url) => originReplace(proxify(url));
      ['pushState','replaceState'].forEach(method => {
        const originMethod = window.history[method].bind(window.history);
        window.history[method] = (state, title, url) => originMethod(state, title, url ? proxify(url) : url);
      });
    
      // 劫持动态请求API
      const originFetch = window.fetch.bind(window);
      window.fetch = (input, init) => {
        if (typeof input === 'string') input = proxify(input);
        if (input instanceof Request) input = new Request(proxify(input.url), input);
        return originFetch(input, init);
      }
      const originXhrOpen = XMLHttpRequest.prototype.open;
      XMLHttpRequest.prototype.open = function(method, url, ...rest) {
        return originXhrOpen.call(this, method, proxify(url), ...rest);
      }
    
      // 启动DOM监听,处理直接通过innerHTML/outerHTML批量插入、没经过createElement的节点
      const observer = new MutationObserver((mutations) => {
        mutations.forEach(mut => {
          mut.addedNodes.forEach(node => {
            if (node.nodeType !== 1) return;
            // 检查当前节点
            ['href','src','action'].forEach(attr => {
              if (node.hasAttribute?.(attr)) node.setAttribute(attr, proxify(node.getAttribute(attr)));
            });
            // 检查所有子节点
            node.querySelectorAll?.('a[href], area[href], iframe[src], img[src], script[src], link[href], form[action], embed[src], audio[src], video[src], source[src]').forEach(el => {
              ['href','src','action'].forEach(attr => {
                if (el.hasAttribute(attr)) el.setAttribute(attr, proxify(el.getAttribute(attr)));
              });
            });
          });
        });
      });
      // 立即启动监听,不需要等DOMContentLoaded
      observer.observe(document.documentElement, { childList: true, subtree: true });
    })();
    
  • 第二层:DOM变更全局监听兜底
    上述垫片中已经集成了MutationObserver逻辑,直接监听整个文档树的节点插入,针对通过innerHTML、insertAdjacentHTML等方式批量插入、不经过createElement方法的节点,遍历检查其URL属性并替换,避免漏网的动态链接。

  • 第三层:Worker侧路由兜底
    在Service Worker的fetch事件处理逻辑里加兜底规则:如果收到的请求是访问代理域名下的非根路径、且URL里没有link参数,直接返回302重定向到/?link=编码后的原请求路径,就算前面两层逻辑有遗漏,也不会出现直接访问代理路径的错误跳转。

    兜底逻辑参考:

    addEventListener('fetch', event => {
      const reqUrl = new URL(event.request.url);
      // 非根路径且没有代理参数,自动重定向到正确的代理格式
      if (reqUrl.pathname !== '/' && !reqUrl.searchParams.get('link')) {
        const targetPath = reqUrl.pathname + reqUrl.search + reqUrl.hash;
        return event.respondWith(Response.redirect(
          `${reqUrl.origin}/?link=${encodeURIComponent(targetPath)}`,
          302
        ));
      }
      // 原有代理处理逻辑
      // ...
    })
    
注意事项
  • 注入的垫片必须放在所有业务脚本、外部资源引用之前,否则会出现劫持时机晚于业务脚本执行,部分动态链接已经生成的问题。
  • 如果原始站点返回的响应有CSP(内容安全策略)限制,需要在Worker侧修改响应头,放开内联脚本的执行权限,避免垫片被浏览器拦截。
  • 如果代理的是固定站点,只需要修改proxify函数里的判断逻辑,把目标站点的绝对路径也纳入代理转换范围即可。

内容的提问来源于stack exchange,提问作者Chris Jones

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.27 06:06:27