Recapture That Power of Setu

| 0 | 总字数 2.3k | 期望阅读时间 9 min

这件事儿还挺有意思的(挠头),水一篇博客。

不是一篇技术博文,这是为了爱和正义与网络安全进行的斗争


因为不知道什么原因,2021/2/3 日,我终于发现是因为一些神秘的问题,进而导致无法自定义 header 的 eagle extension 无法方便地从 pixiv 上下载色图!

盗链这部分就使用 firefox 测试了,比较简单。

这本应该是一个容易解决的问题。我只是想下色图,而下色图只需要在 referer 中添加 https://www.pixiv.net 即可。但是这件事儿落到 eagle extension 上就变复杂了,因为涉及阅读的 eagle extension 源码的问题,还涉及浏览器安全的问题。

首先进到 edge 的插件目录里,找到 eagle 的插件,复制出去,然后进行修改。通过开发者模式进行导入。

观察到下载图片的核心函数,background.jsline: 480(and 380,注意有两处,分别对应拖拽和右键菜单) 左右出现了获取图片 base64 等操作。

ensureEagleIsOpen(function () {
  msg.src = convertSrc(msg.src);
  if (!msg.src) return;
  // console.time("取得图片 base64");
  toDataURL(msg.src, function(base64) {
    // console.timeEnd("取得图片 base64");
    var result = base64 || msg.src;
    var data = {
...

不得不吐槽一句 toDataURL 的可读性简直了,还把 custom 拼错了

其一部分代码是这样的

xhr.onload = function() {
  clearTimeout(timeout);
  var reader = new FileReader();
  reader.onloadend = function() {
    if (!sent) {
      sent = true;
      if (reader.result.indexOf("data:image") > -1) {
        callback(reader.result);
      } else {
        callback(undefined);
      }}}
  reader.readAsDataURL(xhr.response);
};

...

timeout = setTimeout(function () {
  if (!sent) {
    sent = true;
    xhr.abort();
    callback(undefined);
  }
}, timeout);
  1. XMLHttpRequest.onload callback is the function to be executed when the request completes successfully.”

  2. The FileReader.onload property contains an event handler executed when the load event is fired.

总而言之,这段代码通过 toDataURL 来尝试获取对应图片的 base64,若失败,将 URL 再传给主程序处理,而主程序端若接受到 URL,则通过代理再次尝试获取该图片。现在的情况是,两次都失败了,因为两次都没有,也不可能设置 referer

尝试使用服务端的方法,观察到 eagle 有两套 API,一套是官方最近公布的Open API,还有一套是原本用于 extension 和主程序通信的 API,端口号分别为 4159341595。由于无法获取原 API 的信息,只能使用开放的 API。

尝试通过官方文档中的模板测试 eagle 服务端是否能进行下载,结论是不行,eagle 的进度条会卡住最后下载失败。

var data = {
  "url": "https://i.pximg.net/img-original/img/2021/02/02/19/18/33/87484491_p0.jpg",
  "name": "Test",
  "headers": {
    "referer": "https://www.pixiv.net"
  }
};

var requestOptions = {
  method: 'POST',
  body: JSON.stringify(data),
  redirect: 'follow'
};

fetch("http://localhost:41595/api/item/addFromURL", requestOptions)
  .then(response => response.json())
  .then(result => console.log(result))
  .catch(error => console.log('error', error));

我使用其他的链接试了试,似乎 eagle 本身在 Open API 这部分的实现有部分问题,最明显的表现是 添加文件 (0/0)

求求你赶紧把这部分的功能做好吧,我只能自己实现了orz

虽然不知道为什么,不过现在剩下的解法只有通过传递 base64 的方式来下载图片。jQuery 为了安全问题禁止更改 header 中的 referer,于是只能通过服务端的 request 来运行。


这里又因为经验吃亏了,我也测试了通过 fetch 来包含 header 信息,依然不行。通过抓包显示 fetch 根本没有添加上 referer 参数,似乎也是因为 ajax 不允许修改 referer 的问题。

再换方法,根据 stackoverflow 上的办法,可以通过给插件提供 webRequestBlocking 权限来发送修改 referer 参数。

好家伙,依然不行。

chrome.webRequest.onBeforeSendHeaders.addListener(
  function(details) {
    details.requestHeaders.push({name: 'Referer', value: 'https://www.pixiv.net'});
    return {requestHeaders: details.requestHeaders};
  },
  {urls: ["http://i.pximg.net/*"]},
  ["blocking", "requestHeaders"]
);


fetch('http://i.pximg.net/img-original/img/2021/02/02/19/18/33/87484491_p0.jpg')

现在想来,应该就是跨域访问的问题,浏览器阻拦 referer 天经地义,只有我是傻逼和网络安全斗智斗勇。


除此之外,我还考虑了绕开 referer 的办法,即使用(及其古老的)referer killer 的思路,可惜在 2021 年早已行不通。

那么,不考虑和浏览器斗智斗勇,现在的解决办法有几个

  • 搭建“反代”来绕过无法发送带 referer 请求的问题
  • 写油猴脚本,通过油猴脚本来发送带 referer 的请求,已证明可行,但是工程量不小,我有点懒
  • 还有一个,通过 inject code 来让浏览器自己发 referer(后续验证不太可行)

一觉睡了起来,脑子清醒了很多,重新找问题。

查一查 log,发现 eagle 2.0.0 版本更新是在 2021/1/21,而我最后一张图是在 2021/1/21 晚存的。再次查 log 发现,之前的内容都是通过 base64(见后) 下载的

[extension] version: 2.1.8 - Save image: [base64][2090658], source: https://www.pixiv.net/artworks/87159236

所以有两种可能性,eagle 插件更新,edge 更新和 pixiv 外链政策更新导致的问题。

去查 qq 聊天记录,看到 edge 88 更新是在 2021/2/22 日(当时我去吐槽了好好看),现在唯一需要的是一个老版本的 eagle extension 插件。发现 extension 更新是 1.12 日,更不可能了。在之前发送请求的时候,edge 可能会回以缓存,现在的机制变化了。

但是换回老版本的 edge 不太可行,这段思考只能帮助我坚定地确定了,和浏览器斗智斗勇是不可能的,最优雅的解决办法是通过 inject code。那,终于到了写代码的时候了。


敢情我昨天一晚上白忙了啊… 不过也算学了不少东西

草,结果是,inject code 也没能解决问题,依然因为 CORS 限制。目前已知油猴脚本可以突破 CORS 的限制,但是完全不知道是怎么做到的。

通过在原脚本中添加

console.log(msg.src);
chrome.tabs.executeScript(
  tab.id,
  { code: `window.inject_url = "${msg.src}";` },
  () => {
    chrome.tabs.executeScript(
      tab.id, 
      { file: "pixiv.js" }, 
      (res) => {
        console.log("Exec: " + res);
      });
  }
);

可以实现简单的跨域传递参数和返回参数,

脚本中的对应函数如下

(function () {
  console.log("Downloading": window.inject_url);    
  var ret;
  function getPixivImage(url) {
    var xhr = new XMLHttpRequest();
    var timeout = 700;
    var sent = false;
    // From background.js
    xhr.onload = function () {
      clearTimeout(timeout);
      var reader = new FileReader();
      reader.onloadend = function () {
        if (!sent) {
          sent = true;
          if (reader.result.indexOf("data:image") > -1) {
            ret = reader.result;
          } else {
            ret = undefined;
          }
        }
      };
      reader.readAsDataURL(xhr.response);
    };
    xhr.open("GET", url);
    xhr.responseType = "blob";
    xhr.send();

    timeout = setTimeout(function () {
      if (!sent) {
        sent = true;
        xhr.abort();
        ret = undefined;
      }
    }, timeout);
  }
  getPixivImage(window.inject_url);

  return ret;
})();

为了防止 CSRF,这里会不工作也情有可原… 又是一个没有想到的地方。那,估计只能调用油猴脚本了,这操作越来越复杂了。

油猴脚本的 GM_xmlhttpRequest 可以实现跨域访问,但是还不太清楚其原理。

但是从外部访问油猴脚本看起来就不太可能… 最终办法真的只有用油猴和 eagle extension 配合了吗?

  • 使用油猴脚本进行跨域访问,将结果写入全局变量中(使用 messaging 也行)
  • 通过pixiv.js 获取全局变量和油猴脚本对接,实现简单的跨域访问。

这里还需要了解 CORS 的简单请求有关内容。满足一系列条件(懒得ref)的请求为简单请求,而简单请求是先发送信息再检查。但是这回的 GET 中含有 referer,所以依然无法满足简单请求的条件。

这篇文档里描述了 content scripts 在 chromium 88 里已无法跨域。


我开了 bgm 上的讨论


好家伙了,我直接好家伙,最后的解决方式有两个

第一个是,在 bgm 的讨论里有人提到给 eagle 发了 email 请求更改这部分的逻辑,然后… 然后更新版本后问题就解决了。

第二个是添加反代(没想到基于 cloudflare worker 的反代那么简单?!),既然问题已经解决了就不做了。

eagle 新版本的逻辑也很奇怪,会先将文件下载到一个 temp 目录里再让主程序从这个目录 fetch 图片。但是这就导致了一个问题:无法 retry。这是个很简单的问题,只要修改一下 retry 的逻辑就好了,但是 eagle 团队就是没有做好…

而且,什么时候能把设置代理给加回来… 我服气啦

有很多很幼稚的地方和问题,这就当成变强过程中的一篇幼稚的博文吧。

不过在写这篇博文里我有这样的感受,以前学的看似“无用”的东西总会有用武之地。哪怕是我已经放弃了的 CTF 也提供给了不少解决问题的能力。感谢自己写过的每一行代码,感谢自己学过的每一个奇奇怪怪的知识。