在 JavaScript 中发送之前拦截 XHR 并更改请求标头和 url

ami*_*elz 6 javascript xmlhttprequest httprequest request-headers http-headers

我想拦截所有正在发送的 XHR 请求,并在发送请求之前更改它们的 URL 和标头。
发现了这个类似的问题,但那里没有答案。

我尝试了 hooking XMLHttpRequest.prototype.open,但它只能让我访问响应:

(function () {
    var origOpen = XMLHttpRequest.prototype.open;
    XMLHttpRequest.prototype.open = function () {
        console.log(arguments); // prints method ("GET"), URL
        console.log(this); // prints response, responseText, responseURL, status, statusText, and onXXX handlers
        origOpen.apply(this, arguments);
    };
})();
Run Code Online (Sandbox Code Playgroud)

还尝试了 hooking XMLHttpRequest.prototype.setRequestHeader,但它只能让我一一访问正在设置的每个标头值,而且我无法将其与请求的 URL 关联起来:

(function () {
    var origSetRequestHeader = XMLHttpRequest.prototype.setRequestHeader;
    XMLHttpRequest.prototype.setRequestHeader = function (header, value) {
        console.log("header", header);
        console.log("value", value);
        origSetRequestHeader.apply(this, arguments);
    };
})();
Run Code Online (Sandbox Code Playgroud)

我设法挂钩XMLHttpRequest.prototype.send设置自定义标头,但由于我想更改现有标头键,因此它会附加我的新值而不是替换现有值。其他人也遇到了同样的问题:1 , 2 :

(function () {
    var origSend = XMLHttpRequest.prototype.send;
    XMLHttpRequest.prototype.send = function () {
        arguments[1] = myNewUrl; // arguments[1] holds the URL
        this.setRequestHeader('existingHeaderKey', newHeaderValue)
        origSend.apply(this, arguments);
    };
})();
Run Code Online (Sandbox Code Playgroud)

我怎样才能做到这一点?

onk*_*kar 8

XMLHttpRequest (xhr) 接口公开很少的东西。因此,您可以拦截的内容是有限的。

但是,我们可以将 xhr 对象包装在Proxy中并收集数据,直到调用 send 为止。在发送请求之前,我们会在一处修改数据。

const OriginalXHR = XMLHttpRequest;

// wrap the XMLHttpRequest
XMLHttpRequest = function() {
  return new Proxy(new OriginalXHR(), {

    open(method, url, async, username = null, password = null) {
      lg('open');
      // collect URL and HTTP method
      this.modMethod = method;
      this.modUrl = url;

      this.open(...arguments);
    },

    setRequestHeader(name, value) {
      lg('set header');
      if (!this.modReqHeaders) {
        this.modReqHeaders = {};
      }
      // collect headers
      this.modReqHeaders[name] = value;

      // do NOT set headers here. Hold back!
      // this.setRequestHeader(name, value);
    },

    send(body = null) {
      lg('processing request...');
      // do the final processing
      // ...
      // don't forget to set headers
      for (const [name, value] of Object.entries(this.modReqHeaders)) {
        this.setRequestHeader(name, value);
      }

      lg('sending request =>' +
        '\n\t\tmethod: \t' + this.modMethod +
        '\n\t\turl:\t\t' + this.modUrl +
        '\n\t\theaders:\t' + JSON.stringify(this.modReqHeaders));
      this.send(body);
    },

    get(xhr, key) {
      if (!key in xhr) return undefined;

      let value = xhr[key];
      if (typeof value === "function") {
        // if wrapped, use the function in proxy
        value = this[key] || value;
        return (...args) => value.apply(xhr, args);
      } else {
        //return properties
        return value;
      }
    },

    set(xhr, key, value) {
      if (key in xhr) {
        xhr[key] = value;
      }
      return value;
    }
  });
}
console.warn('XMLHttpRequest has been patched!\n XMLHttpRequest: ', XMLHttpRequest);

let url = 'https://baconipsum.com/api/?type=all-meat&sentences=1&start-with-lorem=1';

function getData() {
  console.log('fetching lorem ipsum');
  let xhr = new XMLHttpRequest();
  xhr.responseType = 'json';

  xhr.onreadystatechange = function() {
    if (this.readyState == 4 && this.status == 200) {
      document.getElementById("demo").innerText = this.response[0];
    }
  };
  xhr.open("GET", url, true);
  xhr.setRequestHeader('Referer', 'www.google.com');
  xhr.setRequestHeader('Accept-Encoding', 'x-compress; x-zip')
  xhr.setRequestHeader('Accept-Language', 'de-US,en;q=0.5');
  xhr.send();
}

//fancy logging, looks good in dark mode
function lg(msg) {
  console.log('%c\t Proxy: ' + msg, 'background: #222; color: #bada55');
}
Run Code Online (Sandbox Code Playgroud)
#demo {
  min-height: 100px;
  background-color: wheat;
}
Run Code Online (Sandbox Code Playgroud)
<button onclick="getData()">Get data</button>
<div id="demo"></div>
<p>Note: look in the Developer Console for debug logs</p>
Run Code Online (Sandbox Code Playgroud)

您可以根据您的要求将剩余的 xhr 方法或属性包装在代理处理程序中。
这可能不如 Service Worker。但 Service Worker 有以下缺点:

Service Worker 在 Worker 上下文中运行:因此它没有 DOM 访问权限,并且在与为应用程序提供支持的主 JavaScript 不同的线程上运行,因此它是非阻塞的。它被设计为完全异步;因此,同步 XHR 和 Web Storage 等 API 无法在 Service Worker 内部使用。

出于安全原因,服务工作人员仅通过 HTTPS 运行。修改网络请求后,对中间人攻击的完全开放将是非常糟糕的。在 Firefox 中,Service Worker API 也是隐藏的,当用户处于隐私浏览模式时无法使用。参考