IT

Greasemonkey 스크립트에서 XMLHttpRequests를 대행 수신하려면 어떻게 해야 합니까?

itgroup 2023. 3. 29. 21:24
반응형

Greasemonkey 스크립트에서 XMLHttpRequests를 대행 수신하려면 어떻게 해야 합니까?

Greasemonkey를 사용하여 AJAX 요청 내용을 캡처하고 싶습니다.

이거 할 줄 아는 사람 있어요?

인정된 답변은 거의 맞지만 약간의 개선이 필요할 수 있습니다.

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

apply + arguments를 호출보다 더 선호합니다. 그러면 변경 가능한 모든 인수를 열기 위해 명시적으로 알 필요가 없습니다.

XMLHttpRequest.protype.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 ;

Chrome 55 및 Firefox 50.1.0에서 테스트 완료

내 경우 Firefox에서는 읽기 전용 속성이었던 responseText를 수정하고 싶었기 때문에 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);

안전하지 않은 것을 교체할 수 있습니다.Window.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 개체를 사용한 요구는 스크립트에 의해 추적되지 않습니다.이 한계를 극복하는 일은 있을 수 없습니다.

나는 이것을 어떻게 해야 할지 알아내는데 꽤 많은 시간을 소비했다.처음엔 그냥 오버라이드였어window.fetch하지만 그것은 어떤 이유로 작동을 멈췄다 - 나는 그것이 샌드박스를 시도하고 있는 Tampermonkey와 관련이 있다고 생각한다.window(--) 그리고 저도 시도했습니다.unsafeWindow같은 결과를 얻었습니다.

그래서 더 낮은 수준에서 요청을 무시하려고 했어요XMLHttpRequest(또한 클래스 이름 대문자 소문자 ew...) Sean의 답변은 시작하는 데 도움이 되었지만 가로채기 후 응답을 재정의하는 방법을 보여주지 않았습니다.이하에, 이것을 실시합니다.

let interceptors = [];

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

/*
 * Clear interceptors
 */
export 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();

제안된 솔루션을 기반으로 타이프스크립트 솔루션에서 사용할 수 있는 'xhr-extensions.ts' 파일을 구현했습니다.사용방법:

  1. 솔루션에 코드가 포함된 파일 추가

  2. 이렇게 가져오기

    import { XhrSubscription, subscribToXhr } from "your-path/xhr-extensions";
    
  3. 이렇게 구독하다

    const subscription = subscribeToXhr(xhr => {
      if (xhr.status != 200) return;
      ... do something here.
    });
    
  4. 더 이상 구독이 필요하지 않을 때 구독 취소

    subscription.unsubscribe();
    

'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);

greasemonkey를 사용하여 실행할 수 있을지 모르겠지만 확장을 작성하면 옵서버 서비스와 http-on-in-inspired-response 옵서버를 사용할 수 있습니다.

언급URL : https://stackoverflow.com/questions/629671/how-can-i-intercept-xmlhttprequests-from-a-greasemonkey-script

반응형