如何从 Greasemonkey 脚本拦截 XMLHttpRequest?

问题描述 投票:0回答:7

我想使用 Greasemonkey 捕获 AJAX 请求的内容。

有人知道怎么做吗?

javascript ajax greasemonkey
7个回答
79
投票

接受的答案几乎是正确的,但它可以稍微改进一下:

(function(open) {
    XMLHttpRequest.prototype.open = function() {
        this.addEventListener("readystatechange", function() {
            console.log(this.readyState);
        }, false);
        open.apply(this, arguments);
    };
})(XMLHttpRequest.prototype.open);

更喜欢使用 apply + 参数而不是 call,因为这样你就不必显式地知道所有给 open 的参数可能会改变!


6
投票

如何修改 XMLHttpRequest.prototype.open 或使用替换方法来设置自己的回调并调用原始方法?回调可以做它的事情,然后调用指定的原始代码的回调。

换句话说:

XMLHttpRequest.prototype.realOpen = XMLHttpRequest.prototype.open;

var myOpen = function(method, url, async, user, password) {
    //do whatever mucking around you want here, e.g.
    //changing the onload callback to your own version


    //call original
    this.realOpen (method, url, async, user, password);
}  


//ensure all XMLHttpRequests use our custom open method
XMLHttpRequest.prototype.open = myOpen ;

5
投票

在 Chrome 55 和 Firefox 50.1.0 中测试

在我的例子中,我想修改responseText,它在Firefox中是只读属性,所以我必须包装整个XMLHttpRequest对象。我还没有实现整个 API(特别是 responseType),但它足以用于我拥有的所有库。

用途:

    XHRProxy.addInterceptor(function(method, url, responseText, status) {
        if (url.endsWith('.html') || url.endsWith('.htm')) {
            return "<!-- HTML! -->" + responseText;
        }
    });

代码:

(function(window) {

    var OriginalXHR = XMLHttpRequest;

    var XHRProxy = function() {
        this.xhr = new OriginalXHR();

        function delegate(prop) {
            Object.defineProperty(this, prop, {
                get: function() {
                    return this.xhr[prop];
                },
                set: function(value) {
                    this.xhr.timeout = value;
                }
            });
        }
        delegate.call(this, 'timeout');
        delegate.call(this, 'responseType');
        delegate.call(this, 'withCredentials');
        delegate.call(this, 'onerror');
        delegate.call(this, 'onabort');
        delegate.call(this, 'onloadstart');
        delegate.call(this, 'onloadend');
        delegate.call(this, 'onprogress');
    };
    XHRProxy.prototype.open = function(method, url, async, username, password) {
        var ctx = this;

        function applyInterceptors(src) {
            ctx.responseText = ctx.xhr.responseText;
            for (var i=0; i < XHRProxy.interceptors.length; i++) {
                var applied = XHRProxy.interceptors[i](method, url, ctx.responseText, ctx.xhr.status);
                if (applied !== undefined) {
                    ctx.responseText = applied;
                }
            }
        }
        function setProps() {
            ctx.readyState = ctx.xhr.readyState;
            ctx.responseText = ctx.xhr.responseText;
            ctx.responseURL = ctx.xhr.responseURL;
            ctx.responseXML = ctx.xhr.responseXML;
            ctx.status = ctx.xhr.status;
            ctx.statusText = ctx.xhr.statusText;
        }

        this.xhr.open(method, url, async, username, password);

        this.xhr.onload = function(evt) {
            if (ctx.onload) {
                setProps();

                if (ctx.xhr.readyState === 4) {
                     applyInterceptors();
                }
                return ctx.onload(evt);
            }
        };
        this.xhr.onreadystatechange = function (evt) {
            if (ctx.onreadystatechange) {
                setProps();

                if (ctx.xhr.readyState === 4) {
                     applyInterceptors();
                }
                return ctx.onreadystatechange(evt);
            }
        };
    };
    XHRProxy.prototype.addEventListener = function(event, fn) {
        return this.xhr.addEventListener(event, fn);
    };
    XHRProxy.prototype.send = function(data) {
        return this.xhr.send(data);
    };
    XHRProxy.prototype.abort = function() {
        return this.xhr.abort();
    };
    XHRProxy.prototype.getAllResponseHeaders = function() {
        return this.xhr.getAllResponseHeaders();
    };
    XHRProxy.prototype.getResponseHeader = function(header) {
        return this.xhr.getResponseHeader(header);
    };
    XHRProxy.prototype.setRequestHeader = function(header, value) {
        return this.xhr.setRequestHeader(header, value);
    };
    XHRProxy.prototype.overrideMimeType = function(mimetype) {
        return this.xhr.overrideMimeType(mimetype);
    };

    XHRProxy.interceptors = [];
    XHRProxy.addInterceptor = function(fn) {
        this.interceptors.push(fn);
    };

    window.XMLHttpRequest = XHRProxy;

})(window);

2
投票

您可以使用包装器替换文档中的 unsafeWindow.XMLHttpRequest 对象。一些代码(未测试):

var oldFunction = unsafeWindow.XMLHttpRequest;
unsafeWindow.XMLHttpRequest = function() {
  alert("Hijacked! XHR was constructed.");
  var xhr = oldFunction();
  return {
    open: function(method, url, async, user, password) {
      alert("Hijacked! xhr.open().");
      return xhr.open(method, url, async, user, password);
    }
    // TODO: include other xhr methods and properties
  };
};

但这有一个小问题:Greasemonkey 脚本在页面加载之后执行,因此页面可以在加载序列期间使用或存储原始 XMLHttpRequest 对象,因此在脚本执行之前发出的请求,或者使用真正的 XMLHttpRequest 对象发出的请求不会'不会被您的脚本跟踪。我看不出如何解决这个限制。


2
投票
window.fetch

但由于某种原因停止工作 - 我相信这与 Tampermonkey 尝试沙箱

window
(??) 有关,我也尝试了
unsafeWindow
得到了相同的结果。
所以。我开始考虑在较低级别覆盖请求。 

XMLHttpRequest

(也是类名大写小写ew...) Sean 的回答对入门很有帮助,但没有展示如何在拦截后覆盖响应。下面就是这样做的:

let interceptors = [];

/*
 * Add a interceptor.
 */
const addInterceptor = (interceptor) => {
  interceptors.push(interceptor);
};

/*
 * Clear interceptors
 */
const clearInterceptors = () => {
  interceptors = [];
};


/*
 * XML HTPP requests can be intercepted with interceptors.
 * Takes a regex to match against requests made and a callback to process the response.
 */
const createXmlHttpOverride = (
  open
) => {
  return function (
    method: string,
    url,
    async,
    username,
    password
  ) {
    this.addEventListener(
      "readystatechange",
      function () {
        if (this.readyState === 4) {
          // Override `onreadystatechange` handler, there's no where else this can go.
          // Basically replace the client's with our override for interception.
          this.onreadystatechange = (function (
            originalOnreadystatechange
          ) {
            return function (ev) {
              // Only intercept JSON requests.
              const contentType = this.getResponseHeader("content-type");
              if (!contentType || !contentType.includes("application/json")) {
                return (
                  originalOnreadystatechange &&
                  originalOnreadystatechange.call(this, ev)
                );
              }

              // Read data from response.
              (async function () {
                let success = false;
                let data;
                try {
                  data =
                    this.responseType === "blob"
                      ? JSON.parse(await this.response.text())
                      : JSON.parse(this.responseText);
                  success = true;
                } catch (e) {
                  console.error("Unable to parse response.");
                }
                if (!success) {
                  return (
                    originalOnreadystatechange &&
                    originalOnreadystatechange.call(this, ev)
                  );
                }

                for (const i in interceptors) {
                  const { regex, override, callback } = interceptors[i];

                  // Override.
                  const match = regex.exec(url);
                  if (match) {
                    if (override) {
                      try {
                        data = await callback(data);
                      } catch (e) {
                        logger.error(`Interceptor '${regex}' failed. ${e}`);
                      }
                    }
                  }
                }

                // Override the response text.
                Object.defineProperty(this, "responseText", {
                  get() {
                    return JSON.stringify(data);
                  },
                });

                // Tell the client callback that we're done.
                return (
                  originalOnreadystatechange &&
                  originalOnreadystatechange.call(this, ev)
                );
              }.call(this));
            };
          })(this.onreadystatechange);
        }
      },
      false
    );

    open.call(this, method, url, async, username, password);
  };
};

const main = () => {
  const urlRegex = /providers/; // Match any url with "providers" in the url.

  addInterceptor({
    urlRegex,
    callback: async (_data) => {
      // Replace response data.
      return JSON.parse({ hello: 'world' });
    },
    override: true
  });

  XMLHttpRequest.prototype.open = createXmlHttpOverride(
    XMLHttpRequest.prototype.open
  );
};

main();



0
投票

    将包含代码的文件添加到您的解决方案中
  1. 像这样导入
  2. import { XhrSubscription, subscribToXhr } from "your-path/xhr-extensions";

  3. 像这样订阅
  4. const subscription = subscribeToXhr(xhr => { if (xhr.status != 200) return; ... do something here. });

  5. 当您不再需要订阅时取消订阅
  6. subscription.unsubscribe();

    
    
    
  7. “xhr-extensions.ts”文件的内容

export class XhrSubscription { constructor( private callback: (xhr: XMLHttpRequest) => void ) { } next(xhr: XMLHttpRequest): void { return this.callback(xhr); } unsubscribe(): void { subscriptions = subscriptions.filter(s => s != this); } } let subscriptions: XhrSubscription[] = []; export function subscribeToXhr(callback: (xhr: XMLHttpRequest) => void): XhrSubscription { const subscription = new XhrSubscription(callback); subscriptions.push(subscription); return subscription; } (function (open) { XMLHttpRequest.prototype.open = function () { this.addEventListener("readystatechange", () => { subscriptions.forEach(s => s.next(this)); }, false); return open.apply(this, arguments); }; })(XMLHttpRequest.prototype.open);



-2
投票

© www.soinside.com 2019 - 2024. All rights reserved.