Scriptlets icon indicating copy to clipboard operation
Scriptlets copied to clipboard

Add new scriptlet — `trusted-replace-xhr-response`

Open AdamWr opened this issue 3 years ago • 2 comments

I think that it could be useful in case of some SSAI, like for example here - https://github.com/AdguardTeam/AdguardFilters/issues/44461

Steps to reproduce

  1. Use AdGuard extension which doesn't support $replace rules
  2. Add this rule to user rules (it looks like that it's old exception and I will probably remove it soon, but anyway):
@@||player.theplatform.com^$jsinject,badfilter
  1. Go to - https://www.nbc.com/the-six-million-dollar-man/video/the-privacy-of-the-mind/3762893 US IP may be required
  2. Play video (ads is not blocked)

I guess this link may be useful - https://stackoverflow.com/questions/26447335/how-can-i-modify-the-xmlhttprequest-responsetext-received-by-another-function

Example basing on a code from one of the answers from the link above:

(() => {
  var open_prototype = XMLHttpRequest.prototype.open,
      intercept_response = function (urlpattern, callback) {
    XMLHttpRequest.prototype.open = function () {
      arguments['1'].match(urlpattern) && this.addEventListener('readystatechange', function (event) {
        if (this.readyState === 4) {
          var response = callback(event.target.responseText);
          Object.defineProperty(this, 'response', {
            writable: true
          });
          Object.defineProperty(this, 'responseText', {
            writable: true
          });
          this.response = this.responseText = response;
        }
      });
      return open_prototype.apply(this, arguments);
    };
  };

  intercept_response(/manifest\..*\.theplatform\.com\/.*\/.*\.m3u8\?.*|manifest\..*\.theplatform\.com\/.*\/*\.meta.*/, function (response) {

    var new_response = response.replaceAll(/#EXTINF:.*\n.*tvessaiprod\.nbcuni\.com\/video\/[\s\S]*?#EXT-X-DISCONTINUITY|#EXT-X-VMAP-AD-BREAK[\s\S]*?#EXT-X-ENDLIST/g, '');
    console.log("Test: ", new_response)
    return new_response;
  });
  
})();
player.theplatform.com#%#(()=>{var e,t,s=XMLHttpRequest.prototype.open;e=/manifest\..*\.theplatform\.com\/.*\/.*\.m3u8\?.*|manifest\..*\.theplatform\.com\/.*\/*\.meta.*/,t=function(e){e=e.replaceAll(/#EXTINF:.*\n.*tvessaiprod\.nbcuni\.com\/video\/[\s\S]*?#EXT-X-DISCONTINUITY|#EXT-X-VMAP-AD-BREAK[\s\S]*?#EXT-X-ENDLIST/g,"");return console.log("Test: ",e),e},XMLHttpRequest.prototype.open=function(){return arguments[1].match(e)&&this.addEventListener("readystatechange",function(e){4===this.readyState&&(e=t(e.target.responseText),Object.defineProperty(this,"response",{writable:!0}),Object.defineProperty(this,"responseText",{writable:!0}),this.response=this.responseText=e)}),s.apply(this,arguments)}})();

Or by using Proxy:

(() => {
  const adsURL = /manifest\..*\.theplatform\.com\/.*\/.*\.m3u8\?.*|manifest\..*\.theplatform\.com\/.*\/*\.meta.*/;
  const adsRegex = /#EXTINF:.*\n.*tvessaiprod\.nbcuni\.com\/video\/[\s\S]*?#EXT-X-DISCONTINUITY|#EXT-X-VMAP-AD-BREAK[\s\S]*?#EXT-X-ENDLIST/g;

  const wrapper = async (target, thisArg, args) => {
    const xhrURL = args[1];

    if (typeof xhrURL !== 'string' || xhrURL.length === 0) {
      return Reflect.apply(target, thisArg, args);
    }

    if (xhrURL.match(adsURL)) {
      thisArg.addEventListener('readystatechange', function (event) {
        if (thisArg.readyState === 4) {
          const response = thisArg.response;
          Object.defineProperty(thisArg, 'response', {
            writable: true
          });
          Object.defineProperty(thisArg, 'responseText', {
            writable: true
          });
          const responseWithoutAds = response.replaceAll(adsRegex, "");
          thisArg.response = responseWithoutAds;
          thisArg.responseText = responseWithoutAds;
        }
      });
    }

    return Reflect.apply(target, thisArg, args);
  };

  const handler = {
    apply: wrapper
  };
  window.XMLHttpRequest.prototype.open = new Proxy(window.XMLHttpRequest.prototype.open, handler);
})();

Rule:

player.theplatform.com#%#(()=>{window.XMLHttpRequest.prototype.open=new Proxy(window.XMLHttpRequest.prototype.open,{apply:async(a,b,c)=>{const d=c[1];return"string"!=typeof d||0===d.length?Reflect.apply(a,b,c):(d.match(/manifest\..*\.theplatform\.com\/.*\/.*\.m3u8\?.*|manifest\..*\.theplatform\.com\/.*\/*\.meta.*/)&&b.addEventListener("readystatechange",function(){if(4===b.readyState){const a=b.response;Object.defineProperty(b,"response",{writable:!0}),Object.defineProperty(b,"responseText",{writable:!0});const c=a.replaceAll(/#EXTINF:.*\n.*tvessaiprod\.nbcuni\.com\/video\/[\s\S]*?#EXT-X-DISCONTINUITY|#EXT-X-VMAP-AD-BREAK[\s\S]*?#EXT-X-ENDLIST/g,"");b.response=c,b.responseText=c}}),Reflect.apply(a,b,c))}})})();

AdamWr avatar Mar 21 '22 10:03 AdamWr

Should be rather straightforward to implement, but this one also needs to be a "trusted" scriptlet.

ameshkov avatar Mar 23 '22 09:03 ameshkov

Hi. In fact, that would be very cool! :thumbsup:

However, could we consider the possibility of having in agtrusted- scriptlets, for simplicity and consistency, the equivalent of these (with identical or similar name)? :

  • $replace -> in a scriptlet version, possibly agtrusted-replace (therefore limited to XMLHttpRequest and fetch requests),
  • $hls -> in a scriptlet version, possibly agtrusted-hls (logically limited to XMLHttpRequest and possibly fetch requests).

 


Examples for $hls:

Click to see them …
Removes all segments with the matching URL

||example.org^$hls=\/videoplayback^?*&source=dclk_video_ads :arrow_right_hook: example.org#%#//scriptlet('agtrusted-hls', '\/videoplayback^?*&source=dclk_video_ads')

Achieves more or less the same with a regex instead of a URL pattern

||example.org^$hls=/\/videoplayback\/?\?.*\&source=dclk_video_ads/i

:a: :arrow_right_hook: example.org#%#//scriptlet('agtrusted-hls', '/\/videoplayback\/?\?.*\&source=dclk_video_ads/i') :b: :arrow_right_hook: example.org#%#//scriptlet('agtrusted-hls', '/\/videoplayback\/?\?.*\&source=dclk_video_ads/', 'i')

:bulb: B is prettier but less practical for quick copy/paste manual porting. And also less flexible for the future (addition of parameter).

:warning: Automatic porting during list generation can also be considered, however can be problematic because a proper video ads removal sometimes requires json-prune as a complement (due to the fact that no complete porting of $replace possible, i.e. for JSON data present in files loaded via a simple <script> like webapp-[…].js), so in automated generation it would end up with a half-baked solution.

Removes all segments which have the matching tag

||example.org^$hls=/#UPLYNK-SEGMENT:.*\,ad/t

:a: :arrow_right_hook: example.org#%#//scriptlet('agtrusted-hls', '/#UPLYNK-SEGMENT:.*\,ad/t') :b: :arrow_right_hook: example.org#%#//scriptlet('agtrusted-hls', '/#UPLYNK-SEGMENT:.*\,ad/', 't') (Same remark regarding parameters.)  

Click to see them (whitelisting part) …
"Disable all $hls rules for responses from URLs matching ||example.org^"

@@||example.org^$hls

:arrow_right_hook: example.org#@%#//scriptlet('agtrusted-hls')

"Disable all $hls rules with the value of the hls modifier equal to text for responses from URLs matching ||example.org^"

@@||example.org^$hls=text

:arrow_right_hook: example.org#@%#//scriptlet('agtrusted-hls', 'text')

:arrow_right_hook: example.org#@%#//scriptlet('agtrusted-hls', 'rulescontaining:te') (broader, as an idea of possible evolution :bulb:)


 

:bulb: I tried to keep it simple. However, naturally, an additional parameter (possibly optional for the case of agtrusted-hls, although a really speed optimized HLS auto-detection is not so easy I would say) to limit to certain URLs (e.g. certain specific '.m3u8' URLs only for example) for personal reasons or for optimization reasons remains ideal. This, in addition to other conceivable optimizations, such as the use of: [$domain=example.org,path=/player.html]#%#//scriptlet('agtrusted-hls', '[…]').
:information_source: Just for info, besides a fix to the current $replace system (meanwhile fixed by @AdamWr), I had developed (successfully) during my tests a version modifying XMLHttpRequest.prototype.open end of year 2021 for pluto.tv (used in combination with a json-prune rule). I finally gave up the idea of committing this because with the Manifest V3, this JS rule would not have lasted long … Just a short year, until January 2023. So I was planning instead to make this very request for a scriptlet. Therefore, this issue comes at the right time. Thank you @AdamWr! :thumbsup: :relaxed:

contribucious avatar Jul 12 '22 03:07 contribucious

@AdamWr The video https://www.nbc.com/the-six-million-dollar-man/video/the-privacy-of-the-mind/3762893 is not available anymore. Any other video that can be used as an example?

Bushido1 avatar Apr 10 '23 12:04 Bushido1

I guess that any other free video from the same website. For example this one - https://www.nbc.com/thats-my-jam/video/keke-palmer-saweetie-vs-joel-mchale-william/9000235331

AdamWr avatar Apr 14 '23 16:04 AdamWr