当前位置: 首页 > news >正文

spiderdemo第三题

目录

前言

正文

整个流程

分析

探索

代理脚本

网站小的bug

解混淆

js使用proto文件

加载依赖

获取ChallengeRequest、ChallengeResponse并使用

发送请求于与响应

proto与rust

静态代码生成

动态加载

wasm和proto

加载proto文件

映射message

最后的代码


前言

笔者已经通过,直接给出结果

哈哈哈哈哈哈哈

网址如下

T3-protobuf挑战https://www.spiderdemo.cn/authentication/protobuf_challenge/?challenge_type=protobuf_challenge

正文

整个流程

说实话,笔者现在非常喜欢搞这个流程

  1. 下载js文件
  2. html里面发送请求
  3. mitmproxy代理
  4. 获取数据

而这道题也是如下

分析

什么都不用说,发送请求,如下

发现有个challeng.proto文件

proto的介绍和基础使用-CSDN博客https://blog.csdn.net/qq_41358574/article/details/123903840语言指南 (proto 3) | Protocol Buffers 文档 - ProtoBuf 文档https://protobuf.com.cn/programming-guides/proto3/这好像是一种数据传输方式。

总之,不重要。

可以发现这个proto文件的内容不会发送变化。

还可以发现参数,如下

看不懂,不知道是什么东西,像乱码一样

看一下堆栈的调用

发现两个js文件protobuf_challenge.js和 protobuf_min.js

笔者启动了本地t替换,把 protobuf_min.js格式化了,好看点

不用多说,直接下载下来分别叫,index.js和proto.js

在html文件中加载这两个js文件,看看有什么事情发生吗

控制台有与页面有关的报错,不重要。

再看看堆栈调用的过程

不装了,可以发现一个关键的东西apiGetPageData

在本地文件搜索关键字api,可以发现如下东西

哦,笔者明白了,page显示是页数,challengeType显示是protobuf_challenge这个字符串

为什么是protobuf_challenge,标题写着的。

笔者自信,就是这个。

可以添加一行打印语句

如下

没问题

总之,确定两个参数

探索

笔者在html文件执行看看

    <script type="module">const pageData = await apiGetPageData(1, "");</script>

运行,然后报错

没有发现了proto文件

意料之中,毕竟还没下载这个文件

看请求

63344是pycharm 的内置端口,这个static路径怎么办

这里其实给笔者整懵逼了,该怎么搞出一个static

笔者的路径是这样的,文件路径就是路由路径

但是就算笔者创建一个stataic目录,也不行

如果把T3目录标记成源代码目录,还是不行

总之,有个项目根目录,笔者还考虑一下,怎么搞,

但是,笔者突然想到,

新建一个static目录,pycharm把它作为项目的根目录,目录结构如下

把static作为根目录,创建和下载对应的文件

把html文件也放到static目录下

再次打开,结果如下

哈哈哈哈哈哈哈,成功了,把static作为项目的根目录,有点意思

虽然请求报错了

但是,可以发现,参数

已经有参数了,哈哈哈哈哈哈哈,好好好

但是,笔者突然意思到一个小问题,请求具体在哪里发生的???????


=======行,明天再说,不慌------------------------------


好,继续。

可以发现请求的类型是xhr,而且是在spiderdemo页面的protobuf_challenge.js触发

请求地址是https://www.spiderdemo.cn/authentication/api/protobuf_challenge/page/1/

说白了,在本地的index.js发送了请求,类型是xhr

XMLHttpRequest - Web API | MDNhttps://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest直接在本地index.js中搜关键字XMLHttpRequest,如下

很显然,这个发送的请求。

前面有个url,在本地调用看看,在xhr那个打个断点

可以发现如下东西

ChallengeRequest是什么东西?

就是proto定义的一个类型。还有ChallengeResponse

注意到前面url的东西

有个__p_SKMt_STR_57,这是什么

打印出来看看

发现是一个函数,哦

笔者右键选择对表达式求值或者alt+F8

求个值看看,结果如下

说明__p_SKMt_STR_57(0x231)就是字符串/authe

把全部url进行求值,结果如下

那么显然,这个就是路由,笔者将其发送到本机的8082端口

可以看一下其他东西

__p_SKMt_STR_57(0x238)——open

__p_SKMt_STR_57(0x239)——post

...

直言的说,全部都可以解出来,没必要

直接在最前面添加地址就可以了

代理脚本

启动代理

mitmweb --mode reverse:https://www.spiderdemo.cn -p 8082 -s proxy.py

运行html,

这个响应好像是二进制数据,还看不到

打印出来看看

    <script type="module">const res = await apiGetPageData(1, 'protobuf_challenge');console.log(res)</script>

没问题

还有一个undefined,去官网试试,结果如下

还是undefined,哦,难道是网站有bug?

网站小的bug

哈哈哈哈哈哈哈哈哈哈哈哈,笔者分析了一下,确实是个bug

笔者直接给出关键信息

如下

这个result就是返回的信息,但是最后一个

可以看成是message,其中的数据

response[__p_E748_STR_61(0x258) + __p_E748_STR_61(0x259)] 

是undefined

为什么是undefined

因为__p_E748_STR_61(0x258) + __p_E748_STR_61(0x259)是current_page

但是在proto文件的定义中

只有currentPage,因此出现了bug

笑死,哈哈哈哈哈哈哈哈哈哈

解混淆

首先,笔者在mitmweb中,看到了请求的参数

对照proto中的定义可以看出,四个参数对应的含义

message ChallengeRequest {int32 page = 1;            // 请求的页码string challengetype = 2; // 挑战类型int64 timestamp = 3;       // 时间戳,防止重放攻击string signature = 4;      // 请求签名 
}

看来是对challengetype和signature 进行了加密

其中signature使用了md_sign 这个类中的方法,笔者直接给出解混淆的代码,如下

class md_sign {static F(x, y, z) {return ~x & z | y & x}static G(x, y, z) {return x & y | x & z | y & z}static H(x, y, z) {return x ^ y ^ z}static rot(x, n) {return x << n | x >>> 32 - n}static round1(a, b, c, d, k, s) {return this.rot(a + this.F(b, c, d) + k, s)}static round2(a, b, c, d, k, s) {return this.rot(a + this.G(b, c, d) + k + 0x5a827999, s)}static round3(a, b, c, d, k, s) {return this.rot(a + this.H(b, c, d) + k + 0x6ed9eba1, s)}static padMessage(bytes) {const bits = bytes.length * 8, padded = [...bytes];padded.push(0x80);while ((padded.length % 64) !== 56) padded.push(0);for (let i = 0; i < 8; i++) padded.push(bits >>> i * 8 & 0xff);return padded;}static hash(inputBytes) {const padded = this.padMessage(inputBytes);let A = 0x67452301;let B = 0xefcdab89;let C = 0x98badcfe;let D = 0x10325476;for (let block = 0x0; block < padded.length / 0x40; block++) {const M = new Array(0x10);for (let i = 0x0; i < 0x10; i++) {const offset = block * 0x40 + i * 0x4;M[i] = padded[offset] & 0xff | (padded[offset + 0x1] & 0xff) << 0x8 | (padded[offset + 0x2] & 0xff) << 0x10 | (padded[offset + 0x3] & 0xff) << 0x18}let AA = A;let BB = B;let CC = C;let DD = D;AA = this.round1(AA, BB, CC, DD, M[0x0], 0x3);DD = this.round1(DD, AA, BB, CC, M[0x1], 0x7);CC = this.round1(CC, DD, AA, BB, M[0x2], 0xb);BB = this.round1(BB, CC, DD, AA, M[0x3], 0x13);AA = this.round1(AA, BB, CC, DD, M[0x4], 0x3);DD = this.round1(DD, AA, BB, CC, M[0x5], 0x7);CC = this.round1(CC, DD, AA, BB, M[0x6], 0xb);BB = this.round1(BB, CC, DD, AA, M[0x7], 0x13);AA = this.round1(AA, BB, CC, DD, M[0x8], 0x3);DD = this.round1(DD, AA, BB, CC, M[0x9], 0x7);CC = this.round1(CC, DD, AA, BB, M[0xa], 0xb);BB = this.round1(BB, CC, DD, AA, M[0xb], 0x13);AA = this.round1(AA, BB, CC, DD, M[0xc], 0x3);DD = this.round1(DD, AA, BB, CC, M[0xd], 0x7);CC = this.round1(CC, DD, AA, BB, M[0xe], 0xb);BB = this.round1(BB, CC, DD, AA, M[0xf], 0x13);AA = this.round2(AA, BB, CC, DD, M[0x0], 0x3);DD = this.round2(DD, AA, BB, CC, M[0x4], 0x5);CC = this.round2(CC, DD, AA, BB, M[0x8], 0x9);BB = this.round2(BB, CC, DD, AA, M[0xc], 0xd);AA = this.round2(AA, BB, CC, DD, M[0x1], 0x3);DD = this.round2(DD, AA, BB, CC, M[0x5], 0x5);CC = this.round2(CC, DD, AA, BB, M[0x9], 0x9);BB = this.round2(BB, CC, DD, AA, M[0xd], 0xd);AA = this.round2(AA, BB, CC, DD, M[0x2], 0x3);DD = this.round2(DD, AA, BB, CC, M[0x6], 0x5);CC = this.round2(CC, DD, AA, BB, M[0xa], 0x9);BB = this.round2(BB, CC, DD, AA, M[0xe], 0xd);AA = this.round2(AA, BB, CC, DD, M[0x3], 0x3);DD = this.round2(DD, AA, BB, CC, M[0x7], 0x5);CC = this.round2(CC, DD, AA, BB, M[0xb], 0x9);BB = this.round2(BB, CC, DD, AA, M[0xf], 0xd);/* 第 3 轮 16 步:显式调用 round3,无混淆 */AA = this.round3(AA, BB, CC, DD, M[0x0], 0x3);DD = this.round3(DD, AA, BB, CC, M[0x8], 0x9);CC = this.round3(CC, DD, AA, BB, M[0x4], 0xb);BB = this.round3(BB, CC, DD, AA, M[0xc], 0xf);AA = this.round3(AA, BB, CC, DD, M[0x2], 0x3);DD = this.round3(DD, AA, BB, CC, M[0xa], 0x9);CC = this.round3(CC, DD, AA, BB, M[0x6], 0xb);BB = this.round3(BB, CC, DD, AA, M[0xe], 0xf);AA = this.round3(AA, BB, CC, DD, M[0x1], 0x3);DD = this.round3(DD, AA, BB, CC, M[0x9], 0x9);CC = this.round3(CC, DD, AA, BB, M[0x5], 0xb);BB = this.round3(BB, CC, DD, AA, M[0xd], 0xf);AA = this.round3(AA, BB, CC, DD, M[0x3], 0x3);DD = this.round3(DD, AA, BB, CC, M[0xb], 0x9);CC = this.round3(CC, DD, AA, BB, M[0x7], 0xb);BB = this.round3(BB, CC, DD, AA, M[0xf], 0xf);A = A + AA >>> 0x0;B = B + BB >>> 0x0;C = C + CC >>> 0x0;D = D + DD >>> 0x0}const result = [A & 0xff, A >>> 0x8 & 0xff, A >>> 0x10 & 0xff, A >>> 0x18 & 0xff, B & 0xff, B >>> 0x8 & 0xff, B >>> 0x10 & 0xff, B >>> 0x18 & 0xff, C & 0xff, C >>> 0x8 & 0xff, C >>> 0x10 & 0xff, C >>> 0x18 & 0xff, D & 0xff, D >>> 0x8 & 0xff, D >>> 0x10 & 0xff, D >>> 0x18 & 0xff];return result.map(b => {return b.toString(0x10).padStart(0x2, "0")}).join("")}
}

为什么要给出这个代码,一看,觉得是md5加密,但是,不对劲

看下面

这个是调用其中的hash方法,把时间戳传进去,但是,笔者对123456进行加密

发现不对劲,结果应该是e10adc3949ba59abbe56e057f20f883e

有问题。

因此,笔者就把这个js解混淆,一看,确实不是标准的,

一文详解 MD5 信息摘要算法 - bjxiaxueliang - 博客园https://www.cnblogs.com/xiaxveliang/p/15004954.htmlshifts不是标准的,缺少了round4。

总之,解开了。

challenget_type加密后是surwrexibfkdoohqjh,这好像是个定值,不会变化。


===============就这样,明天再说=============

js使用proto文件

继续搞事情

加载依赖

正好学习一下这个proto,这个东西,怎么使用

前端如何对接protobuf | PingCode智库https://docs.pingcode.com/baike/2563896笔者新建一个html文件,加载依赖protobuf的关键依赖

<script src="https://cdn.bootcdn.net/ajax/libs/protobufjs/7.5.4/protobuf.min.js"></script>

对应proto文件,就使用这道题的challenge.proto

加载proto文件

    <script type="module">const root = await protobuf.load('/static/protos/challenge.proto');</script>

结果如下

没问题。

获取ChallengeRequest、ChallengeResponse并使用

在proto文件,有个package,相当于包的名字,需要使用

即,代码如下

const ChallengeRequest=root.lookup('authentication.ChallengeRequest')
const ChallengeResponse=root.lookup('authentication.ChallengeResponse')
console.log(ChallengeRequest)
console.log(ChallengeResponse)

有点多的属性

在fieldsArray发现了关键的东西

这个ChallengeRequest就类似于rust中的结构体,其中的page、challengetype等就是其中的字段。差不多

现在还没有初始化ChallengeRequest,需要初始化,比如

        const req = ChallengeRequest.create({page: 1,challengetype: 'protobuf_challenge',timestamp: Date.now(),signature: '123456'});

结果如下

发送请求于与响应

使用ChallengeRequest和ChallengeResponse出来请求和响应

直接给出代码

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><title>Title</title><script src="./js/protobuf.min.js"></script><script src="./js/md5.js"></script><script type="module">const root = await protobuf.load('/static/protos/challenge.proto');const ChallengeRequest = root.lookup('authentication.ChallengeRequest')const ChallengeResponse = root.lookup('authentication.ChallengeResponse')let t = Date.now()const req = ChallengeRequest.create({page: 1,challengetype: 'surwrexibfkdoohqjh',timestamp: t.toString(),signature: md_sign.hash(t.toString())});const buffer = ChallengeRequest.encode(req).finish();console.log(buffer instanceof Uint8Array, buffer);const res = await fetch('http://localhost:8082/authentication/api/protobuf_challenge/page/1/', {method: 'POST',headers: {'Content-Type': 'application/x-protobuf'},body: buffer});const respBuf = new Uint8Array(await res.arrayBuffer());const resp = ChallengeResponse.decode(respBuf);console.log(resp)const sum = resp.numbers.reduce((s, item) => s + item.value, 0);console.log(sum)</script>
</head>
<body></body>
</html>

md5.js就是前面解混淆的函数所在的文件

结果如下

没什么问题

使用rust或者python应该也可以搞,笔者懒得搞了,就这样。

proto与rust

按道理来说,这篇博客已经完成了,笔者有其他想法,先使用rust来操作一些proto

新建一个项目

cargo new proto-rs

玩一玩。一些参考

https://protobuf.com.cn/reference/rust/https://protobuf.com.cn/reference/rust/深入掌握 Prost:Rust 中 Protocol Buffers 的高级应用与实战技巧 - 知乎https://zhuanlan.zhihu.com/p/694261734andrewhickman/prost-reflect: A protobuf library extending prost with reflection support and dynamic messages.https://github.com/andrewhickman/prost-reflect

看来可以使用prost-reflect和prost这两个crate。

直接给出Cargo.toml文件

[package]
name = "proto-rs"
version = "0.1.0"
edition = "2024"[dependencies]
serde_json = "1.0.145"
prost-reflect = {version = "0.16.2",features = ["serde","text-format"]}
prost = {version = "0.14.1"}
protox = {version = "0.9.0"}[build-dependencies]
prost-build = {version = "0.14.1"}

静态代码生成

先使用prost来试试

首先,把这个challenge.proto文件放到src/proto目录下

目录如下

然后在build.rs中,其中的代码如下

use std::io::Result;
fn main()-> Result<()> {prost_build::compile_protos(&["src/proto/challenge.proto"],&["src/proto/"])?;println!("cargo:rerun-if-changed=src/proto/challenge.proto");Ok(())
}

构建

cargo build

然后,就可以在target/{debug|release}/build/{crate}-{hash}/out/发现如下文件

就会生成authentication.rs文件,如下

没问题

在main.rs中导入并运行

include!(concat!(env!("OUT_DIR"), "/authentication.rs"));
fn main() {let request = ChallengeRequest {page: 1,challengetype: "protobuf_challenge".to_string(),timestamp: 1234567890123,signature: "666".to_string(),};let n1=NumberData{id:1,value:1,};let n2=NumberData{id:2,value:2,};let resp=ChallengeResponse{numbers:vec![n1,n2],total_pages:100,current_page:1,timestamp:1234567890123,signature:"666".to_string()};println!("request: {:?}", request);println!("resp: {:?}", resp);
}

结果如下,没问题

动态加载

动态加载

这个动态加载还挺麻烦的,笔者学习了一下。

reflect的英文意思是反射,实际上动态加载proto文件,就是使用了反射,类似于java的反射

当然,没有java那么强大。

首先,初始化一个DescriptorPool

let mut pool = DescriptorPool::new();

pool有个方法add_file_descriptor_set

需要传入一个FileDescriptorSet,那么就还需要获取这个

为了获取 FileDescriptorSet,需要使用protox,而protox有一个complie方法

函数签名如下

pub fn compile(files: impl IntoIterator<Item = impl AsRef<Path>>,includes: impl IntoIterator<Item = impl AsRef<Path>>,
) -> Result<prost_types::FileDescriptorSet, Error>

impl IntoIterator表示任何可以转换为迭代器的类型

Item = impl AsRef<Path>表示迭代器产生的每个元素必须实现 AsRef<Path>

成功则返回prost_types::FileDescriptorSet

这就非常的灵活,可以传vec,也可以传 &str,或者传PathBuf

但是传字符串会被作为文件路径去使用

因此,代码如下

let file_descriptor = protox::compile(&["src/proto/challenge.proto"], &["src/proto"])?;

调用add_file_descriptor_set

pool.add_file_descriptor_set(file_descriptor)?;

现在就完成了proto文件的初始化。

后面就使其中的message的初始化,笔者以ChallengeRequest 为例子

还是类似,需要一个东西把ChallengeRequest 反射出来,

首先,在 Protocol Buffers 中,message是一个关键字,用于创建了一个复合数据类型(类似于 Rust 的 struct 或 Java 的 class)。

那么需要把message反射出来,这就需要使用prost-reflect中的MessageDescriptor这个结构体

如何才能获取这个结构体?这就需要使用DescriptorPool中的get_message_by_name这个方法

这一步其实javascript获取proto里面的message差不多,那么rust中的代码如下

    let request_descriptor = pool.get_message_by_name("authentication.ChallengeRequest").ok_or("ChallengeRequest 未找到")?;

这个request_descriptor 的类型就是MessageDescriptor,但是还没完。

这个MessageDescriptor是什么???

MessageDescriptor这个东西,该怎么说,笔者的理解,

就是认为这个MessageDescriptor是一个“工厂”,一个工厂,并不是产品,需要再进一步,

需要生成出产品——DynamicMessage。

因此,代码如下

 let mut request = DynamicMessage::new(request_descriptor);

现在就生成出来这个产品ChallengeRequest ,但是里面什么都没有,需要放东西

而放东西的方法——set_field_by_name

因此,代码如下

    request.set_field_by_name("page", Value::I32(1));request.set_field_by_name("challengetype", Value::String("6666".to_string()));request.set_field_by_name("timestamp", Value::I64(123));request.set_field_by_name("signature", Value::String("signature".to_string()));

至此,才完成了ChallengeRequest 的初始化

总之,还是比较麻烦的。

笔者直接给出全部的代码

use prost::Message;
use prost_reflect::{DescriptorPool, DynamicMessage, Value, ReflectMessage};
use std::collections::HashMap;fn create_dynamic_request() -> Result<String, Box<dyn std::error::Error>> {let mut pool = DescriptorPool::new();let file_descriptor = protox::compile(&["src/proto/challenge.proto"], &["src/proto"])?;pool.add_file_descriptor_set(file_descriptor)?;let request_descriptor = pool.get_message_by_name("authentication.ChallengeRequest").ok_or("ChallengeRequest 未找到")?;let mut request = DynamicMessage::new(request_descriptor);request.set_field_by_name("page", Value::I32(1));request.set_field_by_name("challengetype", Value::String("6666".to_string()));request.set_field_by_name("timestamp", Value::I64(123));request.set_field_by_name("signature", Value::String("signature".to_string()));Ok(serde_json::to_string(&request)?)
}fn main() -> Result<(), Box<dyn std::error::Error>> {println!("=== 动态生成 ChallengeRequest ===");let request = create_dynamic_request()?;println!("{}",request);Ok(())
}

打印结果如下

可以的,不是,有点麻烦的。

wasm和proto

笔者决定再进一步,在wasm里面获取proto文件,然后初始化,编写爬虫,行。


=================先这样吧,明天再说===============


好。

首先,初始化一个wasm项目

wasm-pack new wasm-proto

删除没有用的文件

新建一个examples,里面新建一个html文件,打包wasm项目到examples里面

顺便把static目录和proto文件也复制过来。

笔者使用cargo-make打包,其中Makefile.toml文件的内容如下

[tasks.build-wasm]
command = "wasm-pack"
args = ["build", "--target", "web", "--out-dir", "./examples/pkg", "--no-pack"]

 

打包后,目录如下

后面的流程其实很简单

  1. 初始化wasm
  2. 获取proto文件
  3. 动态加载message,生成参数
  4. 发送请求
  5. 处理响应

首先,考虑如何获取wasm文件,看看地址和路由

可以确定proto文件的路由是/wasm-proto/static/protos/challenge.proto

加载proto文件

其中关键代码如下

async fn get_proto() -> Result<String, JsValue> {let window = window().ok_or("无 window 对象")?;let origin = window.location().origin().map_err(|_| "获取 origin 失败")?;let url = format!("{}/wasm-proto/static/protos/challenge.proto", origin);let content = reqwest::get(&url).await.map_err(|e| JsValue::from_str(&e.to_string()))?.text().await.map_err(|e| JsValue::from_str(&e.to_string()))?;Ok(content)
}#[wasm_bindgen(start)]
pub async fn main(){let result=get_proto().await.map_err(|e| "ggg".to_string()).unwrap();log_1(&JsValue::from_str(&result))
}

#[wasm_bindgen(start)]这个属性宏中有一个参数start,使用了,在初始化的时候就会执行这个函数,就会初始化proto文件

打包,运行,结果如下

可以看到加载了wasm文件和proto文件

也输出了,说明加载没问题


=====明天再说==========


映射message

笔者发现自己考虑的不是很好,笔者发现关键文件,protox这个compile这个方法,需要传入路径,但笔者前面获取的是字符串,那搞了寂寞,使用不了,笔者尝试了,

怎么办?

怎么办?

怎么办?

动态加载的文件不能使用。既然如此,静态操作,使用prost,反正proto文件不会变化的。

静态编译,直接给出结果,如下

在lib.rs中导入

use prost::Message;
include!(concat!(env!("OUT_DIR"), "/authentication.rs"));

那么,后面的事情就不必多说。

最后的代码

直接给出全部代码

lib.rs

use prost::Message;
use protox::Error;
use reqwest::Client;
use wasm_bindgen::prelude::*;
use web_sys::{console::log_1, window};
include!(concat!(env!("OUT_DIR"), "/authentication.rs"));
#[wasm_bindgen(module = "/md5.js")]
extern "C" {type MdSign;// 绑定静态方法 hash#[wasm_bindgen(static_method_of = MdSign)]fn hash(timestamp: String) -> String;
}
fn get_timestamp() -> i64 {#[wasm_bindgen]extern "C" {#[wasm_bindgen(js_namespace = Date)]fn now() -> f64;}now() as i64
}fn generate_signature(timestamp: i64) -> String {MdSign::hash(timestamp.to_string())
}
async fn send_request(page: i32, body: &[u8]) {log_1(&JsValue::from(ChallengeResponse::decode(&mutClient::new().post(format!("http://localhost:8082/authentication/api/protobuf_challenge/page/{page}/")).body(body.to_vec()).send().await.unwrap().bytes().await.unwrap()).unwrap().numbers.iter().map(|n|n.value).sum::<i32>()))
}#[wasm_bindgen(start)]
pub async fn main() {let timestamp = get_timestamp();let signature = generate_signature(timestamp.clone());let request = ChallengeRequest {page: 1,challengetype: "surwrexibfkdoohqjh".to_string(),timestamp,signature,};let mut buf = Vec::new();request.encode(&mut buf).unwrap();send_request(1, &buf).await;
}

目录结构

md5.js的改动

运行,启动代理。结果如下

没问题。

终于完成了。

总结

经过了多天,其实这个proto问题和wasm差不多,都是需要获取某个东西,然后使用这个东西。

就没了。

http://www.dtcms.com/a/610315.html

相关文章:

  • PostgreSQL 实战指南(面向 MySQL 开发者)
  • 山东省建设执业师之家官方网站网站建设培训心得体会
  • 充电桩小程序开发实战:从零到一搭建完整系统【源码+解析+文档】
  • 配置安装mmsegmentation并同步至远程服务器
  • 了解一下Sentry(一个开源的实时错误监控平台)
  • 企业网站建设规划书网站建设制作要学什么软件
  • C#VB.NET中实现可靠的文件监控(新建、删除、改名、内容修改等事件的准确捕获)​
  • Python数据科学与图像处理利器组合:Prophet、Arch、Scikit-image、Pillow-heif用法全解析
  • wordpress 4.6.1海外广告优化师
  • 【运维】GNU/Linux 入门笔记
  • 长沙鞋网站建设煤矿建设工程质量监督总站网站
  • 学做川菜下什么网站爱网站黄
  • 前端自定义右键菜单与图片复制(兼容H5)
  • [Switch大气层]纯净版+特斯拉版 20.5.0大气层1.9.5心悦整合包 固件 工具 插件 前端等switch游戏资源下载合集
  • 同样算法的DFS求解数独C和Python程序用时比较
  • vue3+element-china-area-data 实现省市区三级联动
  • Next.js 项目常见报错排查与解决
  • Vue 校验输入时间与当前时间差大于等于3小时
  • html中网站最下面怎么做设计主题网站
  • 起重机智能选型:从血泪教训到科技护航的革新之路
  • java+maven配置yguard的一次实验
  • 汝南县网站建设Wordpress实现中英文
  • ASC学习笔记0006:游戏效果将如何复制到客户端
  • 延安市住建建设网站无锡网站营销推广
  • 我想做网站服务器选用什么电子商务网站建设总结与体会
  • Oracle分页sql
  • Airsim仿真、无人机、无人车、Lidar、深度强化学习
  • Airsim仿真、无人机、Lidar深度相机、DDPG深度强化学习
  • 做做网站下载2023常熟网站网站建设
  • app推广策略WordPress优化百度广告