本帖最后由 steven026 于 2022-12-22 09:19 编辑
前言
本文为油猴脚本开发指南,提供依靠油猴插件的纯前端基础向WebDAV开发方案。
脚本猫v0.11.0-beta.3及以后版本可以直接用内置的脚本可用的WebDAV api,没必要自己手写,详见CAT_fileStorage示例
由于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
网盘注册完毕获取服务器地址、账户、添加应用后获取密码
坚果云用的是账号密码方式验证,需要记录地址、账号、密码这三个关键信息
查阅脚本猫源码
通过脚本猫源码可知,脚本猫使用的是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是统一标准,难度并不是很大,不如参考其底层通信原理,自己实现封装成油猴脚本。
逆向过程
(主要为分析网络请求,非源码逆向)
打开在脚本猫管理器-工具(这里以0.10.4正式版为例)填入网盘提供的WebDAV地址、用户名、密码,并打开Devtools切换到网络标签卡(以下简称options)
再打开浏览器扩展中脚本猫扩展管理页面检查视图中的background.html并切换到网络标签卡(以下简称background)
在脚本猫管理器中点击一次脚本猫【备份】按钮,在background中查看请求,
并将请求右键复制为fetch,方便查看响应头(受到篇幅限制,这里只保留关键请求头)
可以看到一次备份操作,脚本猫分了3步
fetch("https://dav.jianguoyun.com/dav/", {
"headers": {
"authorization": "Basic **Base64**==",
},
"body": null,
"method": "PROPFIND",
});
第一步"method": "PROPFIND"
访问https://dav.jianguoyun.com/dav/
进行预检
fetch("https://dav.jianguoyun.com/dav/ScriptCat/", {
"headers": {
"authorization": "Basic **Base64**==",
},
"body": null,
"method": "MKCOL",
});
第二步"method": "MKCOL"
访问https://dav.jianguoyun.com/dav/ScriptCat/
新建ScriptCat
目录
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
的文件。
打开网盘的确成功上传了该文件,证明脚本猫的方案可行
尝试实战操作一下,将这三步翻译成GM_xmlhttpRequest
形式,上传内容为hello world
、路径为/hello/world.txt
的文件,可以看见非常成功
(注:此处上传实际上传成功,status:204可能为服务器抽风,201为创建成功、204为删除成功)
至此,完成了预检、创建目录、上传文件3个操作
继续点击脚本猫管理器中【备份列表】按钮,在options中复制请求
可以观察到,获取备份列表进行了2次请求,先预检https://dav.jianguoyun.com/dav/
,再预检https://dav.jianguoyun.com/dav/ScriptCat
然后得到备份列表。说明这2个预检请求能够获取文件列表
观察响应结果,返回的均是xml文档
第一次响应结果仅一些基本信息,没有任何对获取文件列表有用的信息
第二次响应结果第一行<d:response>
返回目录信息,第二行<d:response>
返回文件信息
看来获取备份列表实际上有用的是第二次信息(经测试第一次预检可以省略,直接请求/ScriptCat,/若不存在目录该目录会返回404)
至此,又完成了获取目录文件信息操作
在刚才的备份列表中点击【恢复】按钮,在options中复制请求
可以观察到一个下载操作进行了2次请求,先预检,再直接通过"method":"GET"
下载https://dav.jianguoyun.com/dav/ScriptCat/scriptcat-backup-2022-12-21T16-04-06.zip
,响应内容正是之前上传的内容
继续点击【删除】按钮,在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
// ==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()[0] //获取可下载备份
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似乎没防抖和节流选项
地址、账号、密码一般建议让用户自己注册申请然后填写自己的
不然如果用脚本作者共享的账号、密码在用户过多或者有不会用的用户情况下容易造成频繁或者冗余请求,消耗大量流量,而且开源代码共享账号也容易被恶意利用