steven026 发表于 2022-12-21 16:56:34

[油猴脚本开发指南]利用WebDAV同步脚本数据

本帖最后由 steven026 于 2022-12-22 09:19 编辑

# 前言

本文为油猴脚本开发指南,提供依靠油猴插件的纯前端基础向WebDAV开发方案。
脚本猫v0.11.0-beta.3及以后版本可以直接用内置的脚本可用的WebDAV api,没必要自己手写,详见(https://bbs.tampermonkey.net.cn/thread-3892-1-1.html)

由于WebDAV历史过于古老加之又是统一标准协议,导致商业价值过低,很少有网盘厂商愿意做WebDAV,甚至连一些像样的通俗文档都难以找到,加之传统前端受跨域限制,无法正常通信,导致前端方案都特别少,本文主要通过逆向脚本猫WebDAV方案以学习并开发适用于油猴脚本的方案。
~~(虽然油猴也有WebDAV同步,但由于脚本猫开源、油猴闭源,因此逆向脚本猫更方便)~~
(由于国内支持WebDAV的免费网盘屈指可数,本文选用坚果云WebDAV为例,非广告,其余WebDAV可能无法完全照搬需要稍作变通)

### WebDAV 是什么?

WebDAV是一组基于超文本传输协议的技术集合,有利于用户间协同编辑和管理存储在万维网服务器文档。通俗一点儿来说,WebDAV 就是一种互联网方法,应用此方法可以在服务器上划出一块存储空间,可以使用用户名和密码来控制访问,让用户可以直接存储、下载、编辑文件。

以上介绍出自:https://www.zhihu.com/question/347182171/answer/1603553794

### 为什么选择WebDAV?

正因为WebDAV是统一标准、商业价值低,因此用户体验较好,不会被各大网盘厂商教你怎么用网盘。
不需要用第三方自创的api,基本不受限制,兼容性较好,灵巧便捷。

### 为什么不用脚本管理器自带的WebDAV同步?

油猴、脚本猫等脚本管理器自带的WebDAV不太灵活且操作较繁琐(普通用户可能难以接受),需要先打开脚本管理器选择同步标签再全部备份,再全部下载,再单独选择需要还原的脚本或脚本设置且必须全部覆盖还原不能部分覆盖还原。
而本文的同步脚本数据,主要为直接在脚本目标网站内利用`GM_xmlhttpRequest`,任意指定备份还原非常灵活便捷,不受限制,可以实现全自动上传下载同步,或让用户不用打开脚本管理器直接在页面点击1\~2次按钮进行手动同步。

# 逆向准备

## ~~白嫖~~注册免费WebDAV

!(data/attachment/forum/202212/21/155200wnzcapcpexvvyczy.png)
网盘注册完毕获取服务器地址、账户、添加应用后获取密码
坚果云用的是账号密码方式验证,需要记录地址、账号、密码这三个关键信息

## 查阅脚本猫源码

!(data/attachment/forum/202212/21/153933uz1zzl1wvqjlwwdl.png)
通过脚本猫源码可知,脚本猫使用的是nodejs封装库 https://github.com/perry-mitchell/webdav-client

```
CORS
CORS is a security enforcement technique employed by browsers to ensure requests are executed to and from expected contexts. It can conflict with this library if the target server doesn't return CORS headers when making requests from a browser. It is your responsibility to handle this.

It is a known issue that ownCloud and Nextcloud servers by default don't return friendly CORS headers, making working with this library within a browser context impossible. You can of course force the addition of CORS headers (Apache or Nginx configs) yourself, but do this at your own risk.
```

正如该库README.MD所说,由于传统前端受跨域限制,无法正常使用该库,虽然`GM_xmlhttpRequest`不受跨域限制,但要将该库魔改成油猴可用代价太大,而且WebDAV是统一标准,难度并不是很大,不如参考其底层通信原理,自己实现封装成油猴脚本。

# 逆向过程
(主要为分析网络请求,非源码逆向)
!(data/attachment/forum/202212/21/155904h765t88g2yvy8t28.png)
打开在脚本猫管理器-工具(这里以0.10.4正式版为例)填入网盘提供的WebDAV地址、用户名、密码,并打开Devtools切换到网络标签卡(以下简称options)
!(data/attachment/forum/202212/21/160023kztmd19zqzrk2ssz.png)
再打开浏览器扩展中脚本猫扩展管理页面检查视图中的background.html并切换到网络标签卡(以下简称background)

***

!(data/attachment/forum/202212/21/160445yls2vws477fx6v2v.png)
!(data/attachment/forum/202212/21/160657z8kaee8vyv296v66.png)
在脚本猫管理器中点击一次脚本猫【备份】按钮,在background中查看请求,
并将请求右键复制为fetch,方便查看响应头(受到篇幅限制,这里只保留关键请求头)
可以看到一次备份操作,脚本猫分了3步

```js
fetch("https://dav.jianguoyun.com/dav/", {
"headers": {
    "authorization": "Basic **Base64**==",
},
"body": null,
"method": "PROPFIND",
});
```

第一步`"method": "PROPFIND"`访问`https://dav.jianguoyun.com/dav/`进行预检

```js
fetch("https://dav.jianguoyun.com/dav/ScriptCat/", {
"headers": {
    "authorization": "Basic **Base64**==",
},
"body": null,
"method": "MKCOL",
});
```

第二步`"method": "MKCOL"`访问`https://dav.jianguoyun.com/dav/ScriptCat/`新建`ScriptCat`目录

```js
fetch("https://dav.jianguoyun.com/dav/ScriptCat/scriptcat-backup-2022-12-21T16-04-06.zip", {
"headers": {
    "authorization": "Basic **Base64**==",
    "content-type": "application/octet-stream",
},
"body": "PK\u0005\u0006\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0014\u0000Created by Scriptcat",
"method": "PUT",
});
```

第三步`"method": "PUT"`访问`https://dav.jianguoyun.com/dav/ScriptCat/scriptcat-backup-2022-12-21T16-04-06.zip`,上传文件名为`scriptcat-backup-2022-12-21T16-04-06.zip`,二进制数据为`PK\u0005\u0006\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0000\u0014\u0000Created by Scriptcat`的文件。

***

!(data/attachment/forum/202212/21/161654cch82fi64iuhdzcf.png)
打开网盘的确成功上传了该文件,证明脚本猫的方案可行

***

!(data/attachment/forum/202212/21/162930qrew0oxuu7zus0rk.png)
!(data/attachment/forum/202212/21/162945md7mxdl8o2o88dpz.png)
尝试实战操作一下,将这三步翻译成`GM_xmlhttpRequest`形式,上传内容为`hello world`、路径为`/hello/world.txt`的文件,可以看见非常成功
(注:此处上传实际上传成功,status:204可能为服务器抽风,201为创建成功、204为删除成功)

至此,完成了预检、创建目录、上传文件3个操作

***

!(data/attachment/forum/202212/21/163857f7z7qy92272yir7o.png)
!(data/attachment/forum/202212/21/164251gxhx8yvyhxo3vdyh.png)
继续点击脚本猫管理器中【备份列表】按钮,在options中复制请求
可以观察到,获取备份列表进行了2次请求,先预检`https://dav.jianguoyun.com/dav/`,再预检`https://dav.jianguoyun.com/dav/ScriptCat`
然后得到备份列表。说明这2个预检请求能够获取文件列表
观察响应结果,返回的均是xml文档
第一次响应结果仅一些基本信息,没有任何对获取文件列表有用的信息
第二次响应结果第一行`<d:response>`返回目录信息,第二行`<d:response>`返回文件信息
看来获取备份列表实际上有用的是第二次信息(经测试第一次预检可以省略,直接请求/ScriptCat,/若不存在目录该目录会返回404)
至此,又完成了获取目录文件信息操作

***

!(data/attachment/forum/202212/21/165030bxgg97gzyso9zoy7.png)
!(data/attachment/forum/202212/21/165046xf437g6ngjnn5e66.png)
在刚才的备份列表中点击【恢复】按钮,在options中复制请求
可以观察到一个下载操作进行了2次请求,先预检,再直接通过`"method":"GET"`下载`https://dav.jianguoyun.com/dav/ScriptCat/scriptcat-backup-2022-12-21T16-04-06.zip`,响应内容正是之前上传的内容
!(data/attachment/forum/202212/21/165255qiobav2zwf2wbjab.png)
继续点击【删除】按钮,在options中复制请求
可以观察到一个删除操作也进行了2次请求,先预检,再直接通过`"method":"DELETE"`删除`https://dav.jianguoyun.com/dav/ScriptCat/scriptcat-backup-2022-12-21T16-04-06.zip`

至此所有操作均已完成
(预检目录、新建目录、上传文件、检索目录、下载文件、删除文件)

## 逆向总结

观察一下上述所有请求操作,进行总结共通之处。

### url

可以看到上面所有的请求方式中,请求URL均是以`https://dav.jianguoyun.com/dav/`开头,这正是我们之前获取的WebDAV地址。

### headers

但是之前WebDAV,一共提供了3个信息,地址、账号、密码,现在只用到了1个地址,那么剩下2个账号密码去哪了呢?观察之前的请求,可以看到`headers`中均有一个`authorization": "Basic **Base64**==`,不难看出这就是账号密码验证,`atob()`一下`**Base64**==`,结果正是`` `${账号}:${密码}` ``,因此这条请求头是必加。
(`Basic`是账号密码的验证方式,WebDAV不止账号密码一种验证方式,其余验证方式请求基本类似,这里不再赘述)

### method及status

* 预检目录 `PROPFIND` (正常响应代码207,响应xml文档)
* 新建目录 `MKCOL` (正常响应代码201)
* 上传文件`PUT` (正常响应代码201)
* 检索目录`PROPFIND` (存在响应代码207、不存在响应代码404,响应xml文档)
* 下载文件`GET` (正常响应代码200,响应文件内容)
* 删除文件`DELETE` (正常响应代码204)

# WebDAV Demo

```js
// ==UserScript==
// @name         WebDAV Demo
// @version      1.0
// @description简易油猴WebDAV Demo,可同步任意你想同步的内容,需要预先配置WebDAV地址、账号、密码
// @author       DreamNya
// @match      https://bbs.tampermonkey.net.cn/
// @grant      GM_xmlhttpRequest
// @grant      unsafeWindow
// @license      MIT
// ==/UserScript==

//这里是以账号密码为验证方式,其余验证方式需要做对应修改,Demo中不赘述
const url = '这里填写WebDAV地址';
const username = '这里填写WebDAV账号';
const password = '这里填写WebDAV密码';


//Demo中将同步方法暴露到全局,利用控制台调用,实际可以写UI添加监听事件触发函数
const syncHelper = {}
unsafeWindow.syncHelper = syncHelper;



/*
method预先配置config
*/

//检索目录config
const PROPFIND = {
    method: 'PROPFIND',
    status: 207,
    success: (xhr) => { return xhr.responseText.match(/(?<=<d:displayname>).*?(?=<\/d:displayname>)/g).slice(1) }, //返回文件名
    fail: (xhr) => { if (xhr.status == 404) return GM_xhr(MKCOL) } //目录不存在时新建目录
}
//新建目录config
const MKCOL = {
    method: 'MKCOL',
    status: 201,
    success: () => { return GM_xhr(PROPFIND) } //新建完目录后继续检索
}
//下载文件config
const GET = {
    method: 'GET',
    status: 200,
    success: (xhr) => { return xhr.responseText } //返回下载文件
}
//上传文件config
const PUT = {
    method: 'PUT',
    status: 201,
    success: () => { alert('上传成功') },
    fail: () => { alert('上传失败') }
}
//删除文件config
const DELETE = {
    method: 'DELETE',
    status: 204,
}


/*
简易包装后的方法
*/

//自动下载最新备份
syncHelper.AutoDownload = async function () {
    const fileName = (await this.list()).reverse() //获取可下载备份
    if (!fileName) return alert('无可下载备份')
    const fileData = await this.download(fileName)//下载备份
    await this.restore(fileData) //还原备份
}

//获取可下载文件名 返回数组 旧→新
syncHelper.list = function () {
    return GM_xhr(PROPFIND)
}

//通过指定文件名下载文件
syncHelper.download = function (fileName) {
    const path = '/' + fileName
    return GM_xhr({ ...GET, path })
}

//还原备份
syncHelper.restore = function (fileData) {
    //Your code here...
}


//上传存档
syncHelper.upload = async function () {
    const filename = "备份文件名" + new Date()
    const data = "备份文件内容"
    const path = '/' + filename
    //const headers={"content-type":'text/plain'}
    await GM_xhr({ ...PUT, path, data })
}



/*
请求方法(利用解构减少代码量)
GM_xmlhttpRequest使用方法不再赘述
*/

function GM_xhr({ path = '/', status, success, fail, fuc, headers = {}, ...config }) { //解构赋值
    //if(!url || !username ||!password)throw new Error('配置未填写完整')
    return new Promise(resolve => { //包装异步为同步
      GM_xmlhttpRequest({
            url: url + '/指定目录' + path,
            ...config, //对象解构
            headers: {
                authorization: 'Basic ' + btoa(username + ':' + password), //这里是以账号密码为验证方式
                ...headers //对象解构
            },
            onload: xhr => {
                console.log(xhr)
                let result
                if (!status || xhr.status == status) {
                  if (success) result = success(xhr) //成功后执行的函数
                } else {
                  if (fail) result = fail(xhr) //失败后执行的函数
                }
                if (fuc) fuc(xhr) //无论成功失败均执行的函数
                resolve(result) //返回结果
            }
      })
    })
}
```

注:
Demo利用了一些解构的语法糖以及函数包装减少代码量
之前提到预检目录可以省略,不必每次都请求,Demo直接将预检过程省略,减少请求次数
Demo中的检索目录,只检索文件名,不检索其他信息,所以直接用正则匹配`xhr.responseText`,如果需要完整信息,建议遍历XML document
`new DOMParser().parseFromString(xhr.responseText,'text/xml')`或`xhr.responseXML`
遍历所有`<d:response>`的方法为`xhr.responseXML.querySelectorAll('response')`
(建议使用原生方法,不建议用jQuery,jQuery对XML document支持似乎非常差)

# 完结撒花

注:WebDAV似乎没防抖和节流选项
地址、账号、密码一般建议让用户自己注册申请然后填写自己的
不然如果用脚本作者共享的账号、密码在用户过多或者有不会用的用户情况下容易造成频繁或者冗余请求,消耗大量流量,而且开源代码共享账号也容易被恶意利用

steven026 发表于 2022-12-21 21:03:27

@王一之 @李恒道
排版特别乱有啥好解决的办法嘛0.0
还有通过MD上传错的图片附件太多了,分不清要删除哪些怎么办

王一之 发表于 2022-12-21 21:08:02

steven026 发表于 2022-12-21 21:03
@王一之 @李恒道
排版特别乱有啥好解决的办法嘛0.0
还有通过MD上传错的图片附件太多了,分不清要删除哪些 ...

我现在的解决方案是不管他,后面再加一个一键清理的功能

王一之 发表于 2022-12-21 21:13:48

感觉应该不是油猴脚本开发指南了。。。这个塞的东西越来越奇怪了{:4_102:}

steven026 发表于 2022-12-21 21:15:50

王一之 发表于 2022-12-21 21:13
感觉应该不是油猴脚本开发指南了。。。这个塞的东西越来越奇怪了

哈哈哈哈哈
其实我一开始写这个的主要目的是在家和公司间自动同步之前玩的放置游戏存档和脚本选项
之前都是存在localStorage里的,每次导出导入很麻烦,用微信QQ文件传输也麻烦
不如直接用油猴全自动了

李恒道 发表于 2022-12-21 23:00:50

王一之 发表于 2022-12-21 21:13
感觉应该不是油猴脚本开发指南了。。。这个塞的东西越来越奇怪了

后续我觉得可以搞个类似吾爱的那种论坛精华系列

极品小猫 发表于 2022-12-22 12:32:41

所以WebDAV同步存档的功能终于要来了么……

最近我在重写那个离线挂机,这循环方式会卡线程,而且进度不可见。

为了加个进度UI结果重写整个脚本{:4_89:}

不过现在疫情要照顾娃也没什么时间去进一步测试

现有的脚本工作模式是把代码注入到核心模块,再由核心模块去执行,这导致脚本的API使用起来很麻烦……

steven026 发表于 2022-12-22 13:40:31

极品小猫 发表于 2022-12-22 12:32
所以WebDAV同步存档的功能终于要来了么……

最近我在重写那个离线挂机,这循环方式会卡线程,而且进度不可 ...

同步存档我基本写完了 这两天我自己用的很爽【……
读取界面点一下按钮一键下载覆盖 游戏界面点一下按钮一键上传,然后还打算加个定时备份功能,实现半自动化或者全自动化

至于脚本API
目前思路是需要共享的函数挂到PokeClickerHelper这个对象上,不共享的函数直接放在脚本内部自己调用,避免过多污染对象
哥哥如果想改离线挂机的话,直接在离线模块里面写就行了,我到时候给你个greasyfork脚本权限这样可以直接更新发布了?

至于进度UI 我之前的设想是每10%或者20%进度左上角弹一个提示 后来忘记写了【

极品小猫 发表于 2022-12-22 16:24:42

steven026 发表于 2022-12-22 13:40
同步存档我基本写完了 这两天我自己用的很爽【……
读取界面点一下按钮一键下载覆盖 游戏界面点一下按钮 ...

改完先内部测试一下好不好用再决定吧,主要是这个脚本好像可以不依赖核心脚本

极品小猫 发表于 2022-12-22 16:25:38

主要是我自己也阳了……
页: [1] 2
查看完整版本: [油猴脚本开发指南]利用WebDAV同步脚本数据