某书会议室预定流程分析(protobuf)

分析目标

分析飞书会议室预定的流程,构造请求数据,并分析响应数据,实现会议室预定自动化。

准备工作

浏览器登录飞书日历网页版,添加日程,F12查看请求和响应的数据包以及网页信息:

观察数据包和网页信息,预定会议室的流程首先是查询所有的工区信息,然后再根据工区信息去查询工区会议室信息,将结果通过网页展示给用户,用户选择可用的会议室之后,点击保存按钮即可预定会议室。

难点:可以观察到请求和响应的数据包,不是json数据,而是protobuf序列化的流数据。

关于protobuf:

https://protobuf.dev/

Python 爬虫进阶必备 | 由某知识平台延伸的 Protobuf 协议分析学习

接下来需要做什么?

  1. 首先需要分析构造请求数据和解析响应数据的js代码,得到protobuf的结构文件,然后根据protobuf的结构文件反序列化请求和响应的数据,得到json格式数据;
  2. 分析json数据中各个字段的含义,能够手动构造对应的请求数据。

经过上述两个步骤,即可实现会议室预定的自动化。

JS逆向

根据上述的分析流程,将js逆向工作分为3个部分:

  1. 获取工区信息
  2. 获取会议室信息
  3. 预定会议室

js逆向如何开始/如何找到分析入口?

方法一:

针对本案例中的情况,数据构造和请求是在用户点击按钮之后,所以可以先打一个监听鼠标点击事件的断点,然后点击添加会议室即可触发断点。

触发断点之后,可以观察代码上下文和调用堆栈,当代码上下文中出现protobuf和请求相关的代码或者调用堆栈中出现较多的函数时,可以进一步观察,在代码上下文继续打断点调试,或者查看调用堆栈中的函数的上下文代码,看是否有跟protobuf和请求相关的代码。如果没有相关的信息,可以点击向上的箭头,跳出这个函数,当然也可以单步执行,但是速度会比跳出函数慢一些。通过上述方法可以定位到疑似数据请求的代码,可以点击代码左侧在xt()函数中下断点,再次调试(可以F12关闭开发者工具之后等到网页加载完成再F12调试,这样不用一直卡在调试界面需要手动点击执行js代码)。

可以看到,xt()函数确实是在构造请求,并且还有返回结果。但是xt()函数接收参数,查看调用堆栈,找到构造参数e的函数。同时注意到xt()函数的参数e经过qt(e)调用之后生成新的数组n。鼠标放到变量n上可以看到n中的数据,或者通过控制台将n的值打印出来。

查看调用栈,找到构造xt()函数参数的代码:

根据代码和calendarevents.PullBuildingsRequest等字符串可以推测出encode()是生成protocol序列化请求数据的代码,decode的反序列请求返回的protobuf数据的代码,现在已将定位到数据处理的关键入口代码。

方法二:

定位js逆向开始的入口,还可以全局搜索特殊字符串,如protobuf或者请求相关的字符串:

1
2
"content-type": "application/x-protobuf" 或者 application/x-protobuf
"POST"

这种方法也能定位到分析数据构造的入口代码。如果搜索出的结果较多,可以依次打上断点,再调试一遍,看断在哪处代码,然后根据代码上下文和调用堆栈判断入口代码位置。

获取工区信息

通过上面的分析,获取工区信息先要发送请求,然后再解析响应的数据:

经过打断点调试分析和函数名判断,encode是构造获取工区信息的请求数据的代码,decode是解析服务端返回工区信息的代码,xt()函数将encode所构造的获取工区信息的请求数据加上cmd(cmd用于向服务器指定需要进行的操作,如获取工区信息、保存会议室等),再进行一层封装,构造成Package数据。也就是说,在构造请求是需要构造两次数据包,一次是请求工区信息的数据包,一次是通用的请求数据包。

分析到此处,已经定位到关键的代码,可以在开发者工具中调用encode和decode函数去发送和解析数据。一开始尝试直接调用encode和decode的代码,方便而且工作量很小,但是遇到了诸多困难,比如python中无法调用js代码,即使是使用execjs库,也难以为js代码准备self对象,同时,也尝试过直接在浏览器中调用,但是对于js代码,需要执行到对应代码的上下文中,定义了其中的变量,才能调用,否则就是未定义。还有一点,就是这种方式不够优雅和通用。总结来说,还是需要构造.proto文件。

为什么是构造.proto文件,而不是根据js代码写出对应的构造数据的python代码呢?

是因为protobuf协议有自己的标准和规范,各种语言对protobuf协议的实现效果都是相同的,都是根据.proto文件进行序列化和反序列化。只要构造出.proto文件,就能通过谷歌官方提供的protoc工具根据.proto文件生成对应的python代码,在自己的代码中只需要导入对应的包,就可以进行序列化和反序列化,唯一的难点就是.proto文件。

Packet.proto:

1
2
3
4
5
6
7
8
9
10
syntax = "proto2";

message Packet {
optional string sid = 1;
required int32 payloadType = 2;
required int32 cmd = 3;
optional uint32 status = 4;
required bytes payload = 5;
required string cid = 6;
}

生成Packet_pb2.py文件:

1
D:\Tools\protoc-25.1-win64\bin\protoc.exe --python_out=..\pb2 .\Packet.proto

代码中使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import pb2.Packet_pb2

def request_buildingInfo_encode():
request_data = {
'cmd': 1010009,
'payloadType': 1,
'payload': bytes(),
'cid': utils.generate_cid()
}

request_buildingInfo = pb2.Packet_pb2.Packet()
request_buildingInfo.cmd = request_data.get('cmd')
request_buildingInfo.payloadType = request_data.get('payloadType')
request_buildingInfo.payload = request_data.get('payload')
request_buildingInfo.cid = request_data.get('cid')
return request_buildingInfo.SerializeToString()

.proto文件如何构造?

关键代码是encode和decode,可以将encode理解为序列化,decode理解为反序列化。这里请求工区信息并没有构造太多的数据,所以以解析响应数据为例。

进入decode代码可以单步调试进入:

解析响应的工区信息的decode代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
(function anonymous(Reader, types, util) {
return function PullBuildingsResponse$decode(r, l) {
if (! (r instanceof Reader)) r = Reader.create(r) var c = l === undefined ? r.len: r.pos + l,
m = new this.ctor,
k,
value
while (r.pos < c) {
var t = r.uint32() switch (t >>> 3) {
case 1:
if (m.buildings === util.emptyObject) m.buildings = {}
var c2 = r.uint32() + r.pos
k = ""
value = null
while (r.pos < c2) {
var tag2 = r.uint32() switch (tag2 >>> 3) {
case 1:
k = r.string();
break
case 2:
value = types[0].decode(r, r.uint32()) break
default:
r.skipType(tag2 & 7) break
}
}
m.buildings[k] = value
break
default:
r.skipType(t & 7) break
}
}
return m
}
})

构造的.proto文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
syntax = "proto3";

message ResponseBuildingInfo {
map<string, BuildingInfo> building = 1;
}

message BuildingInfo {
string id = 1;
string name = 2;
string description = 3;
float latitude = 4;
float longitude = 5;
repeated string floors = 6;
int32 weight = 9;
}

在解释构造过程之前,先了解一下protobuf序列化数据的结构,序列化格式包括字段标识字段长度字段值,字段标识本身包含字段编号和字段类型。

字段标识的计算方式如下,字段标识的低三位表示字段数据类型,字段标识 >> 3表示字段标号:

1
2
字段标识 = (字段号 << 3) | 字段类型
字段号 = 字段标识 >> 3

例子:

1
2
3
message Test2 {
optional string b = 2;
}
1
12 07 [74 65 73 74 69 6e 67]

0x12是字段标识,字段编号是2,字段类型是2,表示string;0x07是字段长度为7bytes;[]中的内容是字段值。

具体分析可以参考官方文档:https://protobuf.dev/programming-guides/encoding/

接下来逐一分析js代码:

1.代码首先创建一个Reader对象,用于存储反序列化之后的数据:

1
2
3
4
if (! (r instanceof Reader)) r = Reader.create(r) var c = l === undefined ? r.len: r.pos + l,
m = new this.ctor,
k,
value

2.接下来使用while循环不停读取序列化数据,根据之前了解到的protobuf序列化数据的结构,可以知道下面的代码r.uint32()是在读取32bit无符号的字段标识,然后再t >>> 3得到字段编号,然后再用switch-case去匹配不同的字段编号。

1
var t = r.uint32() switch (t >>> 3)

这里只有一个字段号1,说明这个message(即ResponseBuildingInfo,名字随便起)结构中只有一个字段,如果是其他字段号就跳过:

1
2
default:
r.skipType(t & 7) break

接下来就是调用r.uint32()读取字段长度,然后再解析工区信息:

1
var c2 = r.uint32() + r.pos 

3.调用while循环解析工区信息。var tag2 = r.uint32() switch (tag2 >>> 3) 是读取新的字段标识,然后再switch匹配字段编号,有两个字段编号,其中一个类型是string(),另一个类型是复合类型,就是消息嵌套,通过value = types[0].decode(r, r.uint32()) break代码再次调用decode,说明字段2是一个复合类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (m.buildings === util.emptyObject) m.buildings = {}
var c2 = r.uint32() + r.pos
k = ""
value = null
while (r.pos < c2) {
var tag2 = r.uint32() switch (tag2 >>> 3) {
case 1:
k = r.string();
break
case 2:
value = types[0].decode(r, r.uint32()) break
default:
r.skipType(tag2 & 7) break
}
}
m.buildings[k] = value

可以在调试过程中调用decode打印出解析后的工区信息,更快分析出.proto文件。

这里有一些特殊,就是ResponseBuildingInfo中的数据是map类型。因为字段1是一个string,字段2是一个消息嵌套,取名为BuildingInfo(自定义):

https://protobuf.dev/programming-guides/encoding/#maps

1
2
3
message ResponseBuildingInfo {
map<string, BuildingInfo> building = 1;
}

4.接下来继续分析types[0].decode(r, r.uint32()) ,也就是message BuildingInfo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
(function anonymous(Reader, types, util) {
return function CalendarBuilding$decode(r, l) {
if (! (r instanceof Reader)) r = Reader.create(r) var c = l === undefined ? r.len: r.pos + l,
m = new this.ctor
while (r.pos < c) {
var t = r.uint32() switch (t >>> 3) {
case 1:
m.id = r.string() break
case 2:
m.name = r.string() break
case 3:
m.description = r.string() break
case 4:
m.latitude = r.float() break
case 5:
m.longitude = r.float() break
case 6:
if (! (m.floors && m.floors.length))
m.floors = []
m.floors.push(r.string())
break
case 7:
if (! (m.meetingRooms && m.meetingRooms.length))
m.meetingRooms = []
m.meetingRooms.push(types[6].decode(r, r.uint32()))
break
case 8:
m.isDeleted = r.bool() break
case 9:
m.weight = r.int32() break
case 10:
m.seizeTime = r.int64() break
default:
r.skipType(t & 7) break
}
}
if (!m.hasOwnProperty("id")) throw util.ProtocolError("missing required 'id'", {
instance: m
}) return m
}
})

现在有经验之后可以直接看到嵌套消息BuildingInfo中有10个字段(case1-10),第一个字段是string类型的id,第二个字段是string类型的name,其他字段类似。注意其中字段6 floors是string数组类型,在proto中,repeated 关键字来定义在消息内部重复出现(出现零次或多次)的字段。

1
if (! (m.floors && m.floors.length)) m.floors = [] m.floors.push(r.string()) break
1
repeated string floors = 6;

观察解析之后的工区信息,meetingRooms、isDeleted、seizeTime等这些字段信息为空或者没有,在.proto中可以不写这些字段,或者一些不重要的字段,都可以不写,只写一些比较重要的字段,但是需要注意,字段编号要对应好,比如floors字段之后写weight字段,weight字段的编号要对应好js代码中的9,中间不连续。

1
2
repeated string floors = 6;
int32 weight = 9;

最终得到message BuildingInfo结构如下:

1
2
3
4
5
6
7
8
9
message BuildingInfo {
string id = 1;
string name = 2;
string description = 3;
float latitude = 4;
float longitude = 5;
repeated string floors = 6;
int32 weight = 9;
}

最终构造出解析工区信息的结构体。

现在再来看请求工区信息的数据包,请求工区信息时没有提供任何数据,序列化之后就只有一个空数组:

所以就不需要构造.proto文件,只需要构造好Package数据包的.proto文件即可。构造的代码在qt()函数中,经过qt()函数之后,生成POST请求数据n。

Package需要构造的数据如下:

encode代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
(function anonymous(Writer, types, util) {
return function Packet$encode(m, w) {
if (!w) w = Writer.create();
if (m.sid != null && Object.hasOwnProperty.call(m, "sid"))
w.uint32(10).string(m.sid);
w.uint32(16).int32(m.payloadType);
w.uint32(24).int32(m.cmd);
if (m.status != null && Object.hasOwnProperty.call(m, "status"))
w.uint32(32).uint32(m.status);
if (m.payload != null && Object.hasOwnProperty.call(m, "payload"))
w.uint32(42).bytes(m.payload);
if (m.cid != null && Object.hasOwnProperty.call(m, "cid"))
w.uint32(50).string(m.cid);
if (m.pipeEntity != null && Object.hasOwnProperty.call(m, "pipeEntity"))
types[6].encode(m.pipeEntity, w.uint32(58).fork()).ldelim();
if (m.versionPayloads != null && m.versionPayloads.length) {
for (var i = 0; i < m.versionPayloads.length; ++i)
types[7].encode(m.versionPayloads[i], w.uint32(66).fork()).ldelim();
}
if (m.pipeEntities != null && m.pipeEntities.length) {
for (var i = 0; i < m.pipeEntities.length; ++i)
types[8].encode(m.pipeEntities[i], w.uint32(74).fork()).ldelim();
}
if (
m.waitRetryInterval != null &&
Object.hasOwnProperty.call(m, "waitRetryInterval")
)
w.uint32(80).uint32(m.waitRetryInterval);
if (m.command != null && Object.hasOwnProperty.call(m, "command"))
w.uint32(88).int32(m.command);
if (m.cursor != null && Object.hasOwnProperty.call(m, "cursor"))
w.uint32(96).uint64(m.cursor);
if (m.compactedSids != null && m.compactedSids.length) {
for (var i = 0; i < m.compactedSids.length; ++i)
w.uint32(104).int64(m.compactedSids[i]);
}
return w;
};
});

分析请求的数据包和之前分析响应的数据包类似,请求是先创建一个Writer对象,用于保存序列化的请求数据:

1
w = Writer.create()

接着拼接字段标识和字段类型,其中uint32(10)添加一个32bit的字段标识,string(m.sid)标识sid字段是string类型:

1
2
if (m.sid != null && Object.hasOwnProperty.call(m, "sid"))
w.uint32(10).string(m.sid);

通过字段标识10可以得到字段编号为1:

1
10 >> 3 = 1

这样就可以得到message Packet的第一个字段sid,同样的方法可以得到后面的其他字段:

1
2
3
4
5
6
7
8
9
10
syntax = "proto2";

message Packet {
optional string sid = 1;
required int32 payloadType = 2;
required int32 cmd = 3;
optional uint32 status = 4;
optional bytes payload = 5;
optional string cid = 6;
}

需要注意的是,在proto2中,字段前面可以加上optional或者required来指定字段是可选还是必需,默认(不指定)是可选的;但是在 proto3 中,所有的字段都默认为必需的,不存在 optionalrequired 标识,所有字段如果未设置都会有默认值,默认值将是该类型的零值(例如,对于字符串,零值是空字符串,对于整数,零值是0),并且在编码消息时,如果字段的值为其类型的默认值,则该字段可能不会被包含在编码的消息中。

在构造请求数据时,建议使用proto2版本,因为如果使用proto3,当请求的数据中某个字段为空时,会被省略,但是这个字段又是服务端所必需的,就会产生非预期的效果,算是一个坑点。并且,可以观察到js代码中,有一些字段使用if去判断是否为空,为空就不引用该字段,如sid;但是也有一些字段直接获取,如payloadType,对于proto2,sid就是可选的,payloadType就是必需的:

1
2
3
if (m.sid != null && Object.hasOwnProperty.call(m, "sid"))
w.uint32(10).string(m.sid);
w.uint32(16).int32(m.payloadType);

这个可选还是必需其实无需和js代码一一对应,optional的字段可以是required,但是required的字段不能是optional。

构造好请求和解析的.proto文件之后,就可以使用protoc工具生成python代码,最后即可在自己的python代码中引用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import pb2.Packet_pb2
import pb2.RequestMeetingRoomInfo_pb2
import generate_request_data
import utils

# 获取会议室建筑信息请求数据构造
def request_buildingInfo_encode():
request_data = {
'cmd': 1010009,
'payloadType': 1,
'payload': bytes(),
'cid': utils.generate_cid()
}

request_buildingInfo = pb2.Packet_pb2.Packet()
request_buildingInfo.cmd = request_data.get('cmd')
request_buildingInfo.payloadType = request_data.get('payloadType')
request_buildingInfo.payload = request_data.get('payload')
request_buildingInfo.cid = request_data.get('cid')
return request_buildingInfo.SerializeToString()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import pb2.ResponseBuildingInfo_pb2
import pb2.ResponseMeetingRoomInfo_pb2
import pb2.Packet_pb2
import base64


# 解析工区信息
def response_buildingInfo_decode(response_data):
packet = pb2.Packet_pb2.Packet()
response_buildingInfo = pb2.ResponseBuildingInfo_pb2.ResponseBuildingInfo()

packet.ParseFromString(response_data)
response_buildingInfo.ParseFromString(packet.payload)

# 要将floors(protobufListValue)转换为Python列表
buildingInfo = {key: {
'id': value.id,
'name': value.name,
'description': value.description,
'latitude': value.latitude,
'longitude': value.longitude,
'floors': list(value.floors),
'weight': value.weight
} for key, value in response_buildingInfo.building.items()}

return buildingInfo

解析响应的数据包之前需要先使用Packet进行反序列化。

关于cid的生成:

注意到请求的数据中有一个cid字段,用于服务端区分请求数据。cid在qt()函数中生成:

生成cid的代码比较简单,不需要转换成python代码,直接使用execjs库执行即可:

1
2
3
4
5
6
7
8
9
10
11
def generate_cid():
script = (
'''
get_cid = function() {
return (Math.random().toString(36) + "0000000000").substring(2, 2 + "0000000000".length)
};
'''
)
x = execjs.compile(script)
result = x.call("get_cid")
return result

获取会议室信息

序列化请求会议室信息的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
(function anonymous(Writer, types, util) {
return function PullMeetingRoomsInBuildingRequest$encode(m, w) {
if (!w) w = Writer.create();
if (m.startTime != null && Object.hasOwnProperty.call(m, "startTime"))
w.uint32(8).int64(m.startTime);
if (m.endTime != null && Object.hasOwnProperty.call(m, "endTime"))
w.uint32(16).int64(m.endTime);
if (m.buildingIds != null && m.buildingIds.length) {
for (var i = 0; i < m.buildingIds.length; ++i)
w.uint32(26).string(m.buildingIds[i]);
}
if (m.rrule != null && Object.hasOwnProperty.call(m, "rrule"))
w.uint32(34).string(m.rrule);
if (
m.needDisabledResource != null &&
Object.hasOwnProperty.call(m, "needDisabledResource")
)
w.uint32(40).bool(m.needDisabledResource);
if (
m.startTimezone != null &&
Object.hasOwnProperty.call(m, "startTimezone")
)
w.uint32(50).string(m.startTimezone);
if (
m.meetingRoomFilter != null &&
Object.hasOwnProperty.call(m, "meetingRoomFilter")
)
types[6].encode(m.meetingRoomFilter, w.uint32(58).fork()).ldelim();
if (m.meetingRoomFloorFilter != null && m.meetingRoomFloorFilter.length) {
for (var i = 0; i < m.meetingRoomFloorFilter.length; ++i)
types[7]
.encode(m.meetingRoomFloorFilter[i], w.uint32(66).fork())
.ldelim();
}
return w;
};
});

请求会议室信息的.proto结构文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
syntax = "proto2";

message RequestMeetingRoomInfo {
repeated string buildingIds = 3;
required bool needDisabledResource = 5;
required string startTimezone = 6;
repeated MeetingRoomFilter meetingRoomFilter = 7;
}

message MeetingRoomFilter {
required int32 minCapacity = 1;
repeated string needEquipments = 3;
}

反序列化会议室信息的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
(function anonymous(Reader, types, util) {
return function PullMeetingRoomsInBuildingResponse$decode(r, l) {
if (!(r instanceof Reader)) r = Reader.create(r);
var c = l === undefined ? r.len : r.pos + l,
m = new this.ctor(),
k,
value;
while (r.pos < c) {
var t = r.uint32();
switch (t >>> 3) {
case 1:
if (m.resources === util.emptyObject) m.resources = {};
var c2 = r.uint32() + r.pos;
k = "";
value = null;
while (r.pos < c2) {
var tag2 = r.uint32();
switch (tag2 >>> 3) {
case 1:
k = r.string();
break;
case 2:
value = types[0].decode(r, r.uint32());
break;
default:
r.skipType(tag2 & 7);
break;
}
}
m.resources[k] = value;
break;
default:
r.skipType(t & 7);
break;
}
}
return m;
};
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
(function anonymous(Reader, types, util) {
return function CalendarResource$decode(r, l) {
if (!(r instanceof Reader)) r = Reader.create(r);
var c = l === undefined ? r.len : r.pos + l,
m = new this.ctor(),
k,
value;
while (r.pos < c) {
var t = r.uint32();
switch (t >>> 3) {
case 1:
m.id = r.string();
break;
case 2:
m.calendarId = r.string();
break;
case 3:
m.name = r.string();
break;
case 4:
m.type = r.int32();
break;
case 5:
m.status = r.int32();
break;
case 6:
m.buildingId = r.string();
break;
case 7:
m.description = r.string();
break;
case 8:
m.capacity = r.int32();
break;
case 9:
m.floorName = r.string();
break;
case 10:
m.category = r.string();
break;
case 11:
m.isDeleted = r.bool();
break;
case 12:
m.isExternal = r.bool();
break;
case 13:
m.weight = r.int32();
break;
case 14:
m.tenantId = r.string();
break;
case 15:
m.isDisabled = r.bool();
break;
case 16:
m.resourceSchema = r.bytes();
break;
case 17:
m.schemaExtraData = r.bytes();
break;
case 18:
if (!(m.equipmentNames && m.equipmentNames.length))
m.equipmentNames = [];
m.equipmentNames.push(r.string());
break;
case 19:
if (m.equipmentNameMap === util.emptyObject) m.equipmentNameMap = {};
var c2 = r.uint32() + r.pos;
k = "";
value = "";
while (r.pos < c2) {
var tag2 = r.uint32();
switch (tag2 >>> 3) {
case 1:
k = r.string();
break;
case 2:
value = r.string();
break;
default:
r.skipType(tag2 & 7);
break;
}
}
m.equipmentNameMap[k] = value;
break;
default:
r.skipType(t & 7);
break;
}
}
if (!m.hasOwnProperty("id"))
throw util.ProtocolError("missing required 'id'", { instance: m });
return m;
};
});

解析会议室信息的.proto结构文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
syntax = "proto3";

message ResponseMeetingRoomInfo {
map<string, MeetingRoomInfo> meetingRoom = 1;
}

message MeetingRoomInfo {
string id = 1;
string calendarId = 2;
string name = 3;
int32 type = 4;
int32 status = 5;
string buildingId = 6;
string description = 7;
int32 capacity = 8;
string floorName = 9;
string category = 10;
bool isDeleted = 11;
bool isExternal = 12;
int32 weight = 13;
string tenantId = 14;
bool isDisabled = 15;
bytes resourceSchema = 16;
bytes schemaExtraData = 17;
string equipmentNames = 18;
map<string, string> equipmentNameMap = 19;
}

python代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import pb2.Packet_pb2
import pb2.RequestMeetingRoomInfo_pb2
import generate_request_data
import utils

# 获取会议室信息请求数据构造
def request_meetingRoomInfo_encode(buildingIds):
meetingRoomInfo_request = pb2.RequestMeetingRoomInfo_pb2.RequestMeetingRoomInfo()
# 会议室数据
for i in buildingIds:
meetingRoomInfo_request.buildingIds.append(i)
meetingRoomInfo_request.buildingIds.extend(buildingIds)
meetingRoomInfo_request.needDisabledResource = False
meetingRoomInfo_request.startTimezone = ''
meetingRoomFilter = meetingRoomInfo_request.meetingRoomFilter.add()
meetingRoomFilter.minCapacity = 0
meetingRoomFilter.needEquipments.extend([])
payload = meetingRoomInfo_request.SerializeToString()

# packet请求数据
request_data = {
'cmd': 1010010,
'payloadType': 1,
'payload': payload,
'cid': utils.generate_cid()
}

request_meetingInfo = pb2.Packet_pb2.Packet()
request_meetingInfo.cmd = request_data.get('cmd')
request_meetingInfo.payloadType = request_data.get('payloadType')
request_meetingInfo.payload = request_data.get('payload')
request_meetingInfo.cid = request_data.get('cid')

return request_meetingInfo.SerializeToString()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import pb2.ResponseBuildingInfo_pb2
import pb2.ResponseMeetingRoomInfo_pb2
import pb2.Packet_pb2
import base64

def response_meetingRoomInfo_decode(response_data):
packet = pb2.Packet_pb2.Packet()
response_meetingRoomInfo = pb2.ResponseMeetingRoomInfo_pb2.ResponseMeetingRoomInfo()

packet.ParseFromString(response_data)
response_meetingRoomInfo.ParseFromString(packet.payload)

# 将bytes数据类型转换成base64, map数据转换成python字典
meetingRoomInfo = {key: {
'id': value.id,
'calendarId': value.calendarId,
'name': value.name,
'type': value.type,
'status': value.status,
'buildingId': value.buildingId,
'description': value.description,
'capacity': value.capacity,
'floorName': value.floorName,
'category': value.category,
'isDeleted': value.isDeleted,
'isExternal': value.isExternal,
'weight': value.weight,
'tenantId': value.tenantId,
'isDisabled': value.isDisabled,
'resourceSchema': base64.b64encode(value.resourceSchema).decode('utf-8'),
'schemaExtraData': base64.b64encode(value.schemaExtraData).decode('utf-8'),
'equipmentNames': value.equipmentNames,
'equipmentNameMap': dict(value.equipmentNameMap)
} for key, value in response_meetingRoomInfo.meetingRoom.items()}

return meetingRoomInfo

关于python代码中一些要注意的点:

  1. 可以通过meetingRoomInfo_request.buildingIds.extend(buildingIds)给protobuf字段赋值为数组;
  2. 反序列化protobuf数据时,对于protobuf中的列表和字典字段,需要使用list()和dict()函数转换成python列表和字典。

保存/预定会议室信息

在之前的分析过程中,在发送请求的qt()函数打了断点,点击保存会议室会断下来,查看函数调用栈,可以看到序列化请求数据的位置:

序列化的js代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
(function anonymous(Writer, types, util) {
return function CreateEventRequest$encode(m, w) {
if (!w) w = Writer.create();
if (m.event != null && Object.hasOwnProperty.call(m, "event"))
types[0].encode(m.event, w.uint32(10).fork()).ldelim();
if (
m.notificationType != null &&
Object.hasOwnProperty.call(m, "notificationType")
)
w.uint32(16).int32(m.notificationType);
return w;
};
});

序列化event:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
(function anonymous(Writer, types, util) {
return function WebCalendarEvent$encode(m, w) {
if (!w) w = Writer.create();
if (m.id != null && Object.hasOwnProperty.call(m, "id"))
w.uint32(10).string(m.id);
if (m.key != null && Object.hasOwnProperty.call(m, "key"))
w.uint32(18).string(m.key);
if (m.originalTime != null && Object.hasOwnProperty.call(m, "originalTime"))
w.uint32(24).int64(m.originalTime);
if (m.calendarId != null && Object.hasOwnProperty.call(m, "calendarId"))
w.uint32(34).string(m.calendarId);
if (
m.organizerCalendarId != null &&
Object.hasOwnProperty.call(m, "organizerCalendarId")
)
w.uint32(42).string(m.organizerCalendarId);
if (
m.creatorCalendarId != null &&
Object.hasOwnProperty.call(m, "creatorCalendarId")
)
w.uint32(50).string(m.creatorCalendarId);
if (
m.successorCalendarId != null &&
Object.hasOwnProperty.call(m, "successorCalendarId")
)
w.uint32(58).string(m.successorCalendarId);
if (
m.originalEventKey != null &&
Object.hasOwnProperty.call(m, "originalEventKey")
)
w.uint32(66).string(m.originalEventKey);
if (
m.originalIsAllDay != null &&
Object.hasOwnProperty.call(m, "originalIsAllDay")
)
w.uint32(72).bool(m.originalIsAllDay);
if (m.isFree != null && Object.hasOwnProperty.call(m, "isFree"))
w.uint32(80).bool(m.isFree);
if (m.isAllDay != null && Object.hasOwnProperty.call(m, "isAllDay"))
w.uint32(88).bool(m.isAllDay);
if (
m.isCrossTenant != null &&
Object.hasOwnProperty.call(m, "isCrossTenant")
)
w.uint32(96).bool(m.isCrossTenant);
if (m.isDeleted != null && Object.hasOwnProperty.call(m, "isDeleted"))
w.uint32(104).bool(m.isDeleted);
if (m.lastDate != null && Object.hasOwnProperty.call(m, "lastDate"))
w.uint32(112).int64(m.lastDate);
if (m.source != null && Object.hasOwnProperty.call(m, "source"))
w.uint32(120).int32(m.source);
if (m.color != null && Object.hasOwnProperty.call(m, "color"))
w.uint32(128).int32(m.color);
if (m.visibility != null && Object.hasOwnProperty.call(m, "visibility"))
w.uint32(136).int32(m.visibility);
if (m.version != null && Object.hasOwnProperty.call(m, "version"))
w.uint32(144).int64(m.version);
if (m.status != null && Object.hasOwnProperty.call(m, "status"))
w.uint32(152).int32(m.status);
if (m.rrule != null && Object.hasOwnProperty.call(m, "rrule"))
w.uint32(162).string(m.rrule);
if (
m.guestCanInvite != null &&
Object.hasOwnProperty.call(m, "guestCanInvite")
)
w.uint32(168).bool(m.guestCanInvite);
if (
m.guestCanSeeOtherGuests != null &&
Object.hasOwnProperty.call(m, "guestCanSeeOtherGuests")
)
w.uint32(176).bool(m.guestCanSeeOtherGuests);
if (
m.guestCanModify != null &&
Object.hasOwnProperty.call(m, "guestCanModify")
)
w.uint32(184).bool(m.guestCanModify);
if (
m.selfAttendeeStatus != null &&
Object.hasOwnProperty.call(m, "selfAttendeeStatus")
)
w.uint32(192).int32(m.selfAttendeeStatus);
if (m.startTime != null && Object.hasOwnProperty.call(m, "startTime"))
w.uint32(200).int64(m.startTime);
if (
m.startTimezone != null &&
Object.hasOwnProperty.call(m, "startTimezone")
)
w.uint32(210).string(m.startTimezone);
if (m.endTime != null && Object.hasOwnProperty.call(m, "endTime"))
w.uint32(216).int64(m.endTime);
if (m.endTimezone != null && Object.hasOwnProperty.call(m, "endTimezone"))
w.uint32(226).string(m.endTimezone);
if (m.summary != null && Object.hasOwnProperty.call(m, "summary"))
w.uint32(234).string(m.summary);
if (m.description != null && Object.hasOwnProperty.call(m, "description"))
w.uint32(242).string(m.description);
if (
m.docsDescription != null &&
Object.hasOwnProperty.call(m, "docsDescription")
)
w.uint32(250).string(m.docsDescription);
if (m.locations != null && m.locations.length) {
for (var i = 0; i < m.locations.length; ++i)
types[31].encode(m.locations[i], w.uint32(258).fork()).ldelim();
}
if (m.attendees != null && m.attendees.length) {
for (var i = 0; i < m.attendees.length; ++i)
types[32].encode(m.attendees[i], w.uint32(266).fork()).ldelim();
}
if (m.reminders != null && m.reminders.length) {
for (var i = 0; i < m.reminders.length; ++i)
types[33].encode(m.reminders[i], w.uint32(274).fork()).ldelim();
}
if (m.createTime != null && Object.hasOwnProperty.call(m, "createTime"))
w.uint32(280).int64(m.createTime);
if (m.updateTime != null && Object.hasOwnProperty.call(m, "updateTime"))
w.uint32(288).int64(m.updateTime);
if (m.organizer != null && Object.hasOwnProperty.call(m, "organizer"))
types[36].encode(m.organizer, w.uint32(298).fork()).ldelim();
if (m.creator != null && Object.hasOwnProperty.call(m, "creator"))
types[37].encode(m.creator, w.uint32(306).fork()).ldelim();
if (m.successor != null && Object.hasOwnProperty.call(m, "successor"))
types[38].encode(m.successor, w.uint32(314).fork()).ldelim();
if (m.type != null && Object.hasOwnProperty.call(m, "type"))
w.uint32(320).int32(m.type);
if (m.attachments != null && m.attachments.length) {
for (var i = 0; i < m.attachments.length; ++i)
types[40].encode(m.attachments[i], w.uint32(330).fork()).ldelim();
}
if (m.attendeeInfo != null && Object.hasOwnProperty.call(m, "attendeeInfo"))
types[41].encode(m.attendeeInfo, w.uint32(338).fork()).ldelim();
if (m.schema != null && Object.hasOwnProperty.call(m, "schema"))
types[42].encode(m.schema, w.uint32(346).fork()).ldelim();
if (
m.attendeeSource != null &&
Object.hasOwnProperty.call(m, "attendeeSource")
)
w.uint32(352).int32(m.attendeeSource);
if (
m.meetingMinuteUrl != null &&
Object.hasOwnProperty.call(m, "meetingMinuteUrl")
)
w.uint32(362).string(m.meetingMinuteUrl);
if (
m.inviteOperatorId != null &&
Object.hasOwnProperty.call(m, "inviteOperatorId")
)
w.uint32(370).string(m.inviteOperatorId);
if (m.videoMeeting != null && Object.hasOwnProperty.call(m, "videoMeeting"))
types[46].encode(m.videoMeeting, w.uint32(378).fork()).ldelim();
if (m.schemaV2 != null && Object.hasOwnProperty.call(m, "schemaV2"))
w.uint32(386).bytes(m.schemaV2);
if (m.videoConfig != null && Object.hasOwnProperty.call(m, "videoConfig"))
types[48].encode(m.videoConfig, w.uint32(394).fork()).ldelim();
if (
m.encryptionInfo != null &&
Object.hasOwnProperty.call(m, "encryptionInfo")
)
types[49].encode(m.encryptionInfo, w.uint32(402).fork()).ldelim();
if (m.isWebinar != null && Object.hasOwnProperty.call(m, "isWebinar"))
w.uint32(408).bool(m.isWebinar);
return w;
};
});

构造Location:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
(function anonymous(Writer, types, util) {
return function CalendarEventLocation$encode(m, w) {
if (!w) w = Writer.create();
if (m.name != null && Object.hasOwnProperty.call(m, "name"))
w.uint32(10).string(m.name);
if (m.address != null && Object.hasOwnProperty.call(m, "address"))
w.uint32(18).string(m.address);
if (m.type != null && Object.hasOwnProperty.call(m, "type"))
w.uint32(24).int32(m.type);
if (m.latitude != null && Object.hasOwnProperty.call(m, "latitude"))
w.uint32(37).float(m.latitude);
if (m.longitude != null && Object.hasOwnProperty.call(m, "longitude"))
w.uint32(45).float(m.longitude);
return w;
};
});

构造Attendee:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
(function anonymous(Writer, types, util) {
return function WebCalendarEventAttendee$encode(m, w) {
if (!w) w = Writer.create();
if (m.id != null && Object.hasOwnProperty.call(m, "id"))
w.uint32(10).string(m.id);
if (m.key != null && Object.hasOwnProperty.call(m, "key"))
w.uint32(18).string(m.key);
if (m.originalTime != null && Object.hasOwnProperty.call(m, "originalTime"))
w.uint32(24).int64(m.originalTime);
if (m.displayName != null && Object.hasOwnProperty.call(m, "displayName"))
w.uint32(34).string(m.displayName);
if (
m.attendeeCalendarId != null &&
Object.hasOwnProperty.call(m, "attendeeCalendarId")
)
w.uint32(42).string(m.attendeeCalendarId);
if (m.isOrganizer != null && Object.hasOwnProperty.call(m, "isOrganizer"))
w.uint32(48).bool(m.isOrganizer);
if (m.isOptional != null && Object.hasOwnProperty.call(m, "isOptional"))
w.uint32(56).bool(m.isOptional);
if (m.status != null && Object.hasOwnProperty.call(m, "status"))
w.uint32(64).int32(m.status);
if (
m.inviterCalendarId != null &&
Object.hasOwnProperty.call(m, "inviterCalendarId")
)
w.uint32(74).string(m.inviterCalendarId);
if (m.avatarKey != null && Object.hasOwnProperty.call(m, "avatarKey"))
w.uint32(82).string(m.avatarKey);
if (m.category != null && Object.hasOwnProperty.call(m, "category"))
w.uint32(88).int32(m.category);
if (m.fsUnit != null && Object.hasOwnProperty.call(m, "fsUnit"))
w.uint32(98).string(m.fsUnit);
if (
m.attendeeSchema != null &&
Object.hasOwnProperty.call(m, "attendeeSchema")
)
w.uint32(106).bytes(m.attendeeSchema);
if (
m.schemaExtraData != null &&
Object.hasOwnProperty.call(m, "schemaExtraData")
)
w.uint32(114).bytes(m.schemaExtraData);
if (m.user != null && Object.hasOwnProperty.call(m, "user"))
types[14].encode(m.user, w.uint32(802).fork()).ldelim();
if (m.group != null && Object.hasOwnProperty.call(m, "group"))
types[15].encode(m.group, w.uint32(810).fork()).ldelim();
if (m.resource != null && Object.hasOwnProperty.call(m, "resource"))
types[16].encode(m.resource, w.uint32(818).fork()).ldelim();
if (
m.thirdPartyUser != null &&
Object.hasOwnProperty.call(m, "thirdPartyUser")
)
types[17].encode(m.thirdPartyUser, w.uint32(826).fork()).ldelim();
return w;
};
});

.proto文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
syntax = "proto2";

message Locations {
required string name = 1;
required string address = 2;
required float latitude = 4;
required float longitude = 5;
}

message Attendees {
required string id = 1;
required string key = 2;
required int64 originalTime = 3;
required string displayName = 4;
required string attendeeCalendarId = 5;
required bool isOrganizer = 6;
required bool isOptional = 7;
required int32 status = 8;
required string inviterCalendarId = 9;
required string avatarKey = 10;
required int32 category = 11;
repeated User user = 100;
repeated Resource resource = 102;

}


message User {
required string userId = 1;
required string tenantId = 2;
}

message Resource {
required string tenantId = 1;
required bool isDisabled = 2;
}



message Reminders {
required string calendarEventId = 1;
required int32 minutes = 2;
required int32 method = 3;
}


message Event {
required string id = 1;
required string key = 2;
required int64 originalTime = 3;
required string calendarId = 4;
required string organizerCalendarId = 5;
required string creatorCalendarId = 6;
required string successorCalendarId = 7;
required string originalEventKey = 8;
required bool originalIsAllDay = 9;
required bool isFree = 10;
required bool isAllDay = 11;
required bool isCrossTenant = 12;
required int32 source = 15;
required int32 color = 16;
required int32 visibility = 17;
required int64 version = 18;
required string rrule = 20;
required bool guestCanInvite = 21;
required bool guestCanSeeOtherGuests = 22;
required bool guestCanModify = 23;
required int32 selfAttendeeStatus = 24;
required int64 startTime = 25;
required string startTimezone = 26;
required int64 endTime = 27;
required string endTimezone = 28;
required string summary = 29;
required string description = 30;
required string docsDescription = 31;

repeated Locations locations = 32;
repeated Attendees attendees = 33;
repeated Reminders reminders = 34;

required int64 createTime = 35;
required int64 updateTime = 36;
}

message CreateEventRequest {
required Event event = 1;
}

在调试过程中看到请求的数据,发现Attendees中既有User又有Resource:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
attendees: [
{
"id": "0",
"key": "0",
"originalTime": "0",
"displayName": "xxx",
"attendeeCalendarId": "7251xxx921",
"isOrganizer": false,
"isOptional": false,
"status": "ACCEPT",
"inviterCalendarId": "",
"avatarKey": "v2_cxxxc899d71bg",
"category": "USER",
"user": {
"userId": "7251xxx26",
"tenantId": "687xxx0322"
}
},
{
"id": "",
"key": "",
"originalTime": "0",
"displayName": "1层-D1xxxD座",
"attendeeCalendarId": "7002xxx5713",
"isOrganizer": false,
"isOptional": false,
"status": "NEEDS_ACTION",
"inviterCalendarId": "",
"avatarKey": "",
"category": "RESOURCE",
"resource": {
"tenantId": "687xxx322",
"isDisabled": false
}
}
]

根据js代码,只需按照之前的规则写好User和Resource字段即可,注意user的字段号是100(802 >> 3 = 100),resource的字段号是102(818 >> 3 = 102),还需要注意,在attendee中,user和resource只出现其中一个,另一个不出现,所以在User和Resource字段之前需要加上repeated关键字:

1
2
3
4
5
6
if (m.user != null && Object.hasOwnProperty.call(m, "user"))
types[14].encode(m.user, w.uint32(802).fork()).ldelim();
if (m.group != null && Object.hasOwnProperty.call(m, "group"))
types[15].encode(m.group, w.uint32(810).fork()).ldelim();
if (m.resource != null && Object.hasOwnProperty.call(m, "resource"))
types[16].encode(m.resource, w.uint32(818).fork()).ldelim();
1
2
repeated User user = 100;
repeated Resource resource = 102;

python代码:

构造Package:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import pb2.Packet_pb2
import pb2.RequestMeetingRoomInfo_pb2
import generate_request_data
import utils

# 保存会议室请求数据构造
def request_saveMeetting_encode(meetingInfo):
payload = generate_request_data.CreateEventRequest_encode(meetingInfo)

packet_data = {
'cmd': 1002003,
'payloadType': 1,
'payload': payload,
'cid': utils.generate_cid()
}

packet_request = pb2.Packet_pb2.Packet()
packet_request.cmd = packet_data.get('cmd')
packet_request.payloadType = packet_data.get('payloadType')
packet_request.payload = packet_data.get('payload')
packet_request.cid = packet_data.get('cid')

return packet_request.SerializeToString()

构造payload:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
import pb2.CreateEventRequest_pb2


def CreateEventRequest_encode(data):
event_request = pb2.CreateEventRequest_pb2.CreateEventRequest()
if 'event' in data:
event_request_event = event_request.event
print(event_request_event)
WebCalendarEvent_encode(data.get('event'), event_request_event)
if 'notificationType' in data:
event_request.notificationType = data.get('notificationType')

# 序列化,将message转换为bytes
return event_request.SerializeToString()


def WebCalendarEvent_encode(event, event_request_event):
if 'id' in event:
event_request_event.id = event.get('id')
if 'key' in event:
event_request_event.key = event.get('key')
if 'originalTime' in event:
event_request_event.originalTime = event.get('originalTime')
if 'calendarId' in event:
event_request_event.calendarId = event.get('calendarId')
if 'organizerCalendarId' in event:
event_request_event.organizerCalendarId = event.get('organizerCalendarId')
if 'creatorCalendarId' in event:
event_request_event.creatorCalendarId = event.get('creatorCalendarId')
if 'successorCalendarId' in event:
event_request_event.successorCalendarId = event.get('successorCalendarId')
if 'originalEventKey' in event:
event_request_event.originalEventKey = event.get('originalEventKey')
if 'originalIsAllDay' in event:
event_request_event.originalIsAllDay = event.get('originalIsAllDay')
if 'isFree' in event:
event_request_event.isFree = event.get('isFree')
if 'isAllDay' in event:
event_request_event.isAllDay = event.get('isAllDay')
if 'isCrossTenant' in event:
event_request_event.isCrossTenant = event.get('isCrossTenant')
if 'isDeleted' in event:
event_request_event.isDeleted = event.get('isDeleted')
if 'lastDate' in event:
event_request_event.lastDate = event.get('lastDate')
if 'source' in event:
event_request_event.source = event.get('source')
if 'color' in event:
event_request_event.color = event.get('color')
if 'visibility' in event:
event_request_event.visibility = event.get('visibility')
if 'version' in event:
event_request_event.version = event.get('version')
if 'status' in event:
event_request_event.status = event.get('status')
if 'rrule' in event:
event_request_event.rrule = event.get('rrule')
if 'guestCanInvite' in event:
event_request_event.guestCanInvite = event.get('guestCanInvite')
if 'guestCanSeeOtherGuests' in event:
event_request_event.guestCanSeeOtherGuests = event.get('guestCanSeeOtherGuests')
if 'guestCanModify' in event:
event_request_event.guestCanModify = event.get('guestCanModify')
if 'selfAttendeeStatus' in event:
event_request_event.selfAttendeeStatus = event.get('selfAttendeeStatus')
if 'startTime' in event:
event_request_event.startTime = event.get('startTime')
if 'startTimezone' in event:
event_request_event.startTimezone = event.get('startTimezone')
if 'endTime' in event:
event_request_event.endTime = event.get('endTime')
if 'endTimezone' in event:
event_request_event.endTimezone = event.get('endTimezone')
if 'summary' in event:
event_request_event.summary = event.get('summary')
if 'description' in event:
event_request_event.description = event.get('description')
if 'docsDescription' in event:
event_request_event.docsDescription = event.get('docsDescription')

if 'locations' in event:
for location in event.get('locations'):
event_request_event_location = event_request_event.locations.add()
CalendarEventLocation_encode(location, event_request_event_location)

if 'attendees' in event:
for attende in event.get('attendees'):
event_request_event_attende = event_request_event.attendees.add()
WebCalendarEventAttendee_encode(attende, event_request_event_attende)

if 'reminders' in event:
for reminder in event.get('reminders'):
event_request_event_reminder = event_request_event.reminders.add()
Reminder_encode(reminder, event_request_event_reminder)

if 'createTime' in event:
event_request_event.createTime = event.get('createTime')
if 'updateTime' in event:
event_request_event.updateTime = event.get('updateTime')


def CalendarEventLocation_encode(location, event_request_event_location):
if 'name' in location:
event_request_event_location.name = location.get('name')
if 'address' in location:
event_request_event_location.address = location.get('address')
if 'type' in location:
event_request_event_location.type = location.get('type')
if 'latitude' in location:
event_request_event_location.latitude = location.get('latitude')
if 'longitude' in location:
event_request_event_location.longitude = location.get('longitude')


def WebCalendarEventAttendee_encode(attende, event_request_event_attende):
if 'id' in attende:
event_request_event_attende.id = attende.get('id')
if 'key' in attende:
event_request_event_attende.key = attende.get('key')
if 'originalTime' in attende:
event_request_event_attende.originalTime = int(attende.get('originalTime'))
if 'displayName' in attende:
event_request_event_attende.displayName = attende.get('displayName')
if 'attendeeCalendarId' in attende:
event_request_event_attende.attendeeCalendarId = attende.get('attendeeCalendarId')
if 'isOrganizer' in attende:
event_request_event_attende.isOrganizer = attende.get('isOrganizer')
if 'isOptional' in attende:
event_request_event_attende.isOptional = attende.get('isOptional')
if 'status' in attende:
event_request_event_attende.status = attende.get('status')
if 'inviterCalendarId' in attende:
event_request_event_attende.inviterCalendarId = attende.get('inviterCalendarId')
if 'avatarKey' in attende:
event_request_event_attende.avatarKey = attende.get('avatarKey')
# category类型是int32
if 'category' in attende:
event_request_event_attende.category = attende.get('category')
if 'fsUnit' in attende:
event_request_event_attende.fsUnit = attende.get('fsUnit')
if 'attendeeSchema' in attende:
event_request_event_attende.attendeeSchema = attende.get('attendeeSchema')
if 'schemaExtraData' in attende:
event_request_event_attende.schemaExtraData = attende.get('schemaExtraData')

if 'user' in attende:
event_request_event_attende_user = event_request_event_attende.user.add()
User_encode(attende.get('user'), event_request_event_attende_user)

if 'resource' in attende:
event_request_event_attende_resource = event_request_event_attende.resource.add()
Resource_encode(attende.get('resource'), event_request_event_attende_resource)


def User_encode(user, event_request_event_attende_user):
if 'userId' in user:
event_request_event_attende_user.userId = user.get('userId')
if 'tenantId' in user:
event_request_event_attende_user.tenantId = user.get('tenantId')


def Resource_encode(resource, event_request_event_attende_resource):
if 'tenantId' in resource:
event_request_event_attende_resource.tenantId = resource.get('tenantId')
if 'isDisabled' in resource:
event_request_event_attende_resource.isDisabled = resource.get('isDisabled')


def Reminder_encode(reminder, event_request_event_reminder):
if 'calendarEventId' in reminder:
event_request_event_reminder.calendarEventId = reminder.get('calendarEventId')
if 'minutes' in reminder:
event_request_event_reminder.minutes = reminder.get('minutes')
if 'method' in reminder:
event_request_event_reminder.method = reminder.get('method')

构造的原始数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
event_data = {
"event": {
"locations": [
{
"name": "",
"address": "",
"latitude": 360,
"longitude": 360
}
],
"attendees": [
{
"id": "0",
"key": "0",
"originalTime": "0",
"displayName": "xxx",
"attendeeCalendarId": "xxx",
"isOrganizer": False,
"isOptional": False,
"status": 2,
"inviterCalendarId": "",
"avatarKey": "xxx-dfxxx",
"category": 1,
"user": {
"userId": "xxx",
"tenantId": meeting.get('tenantId')
}
},
{
"id": "",
"key": "",
"originalTime": "0",
"displayName": meeting.get('name'),
"attendeeCalendarId": meeting.get('calendarId'),
"isOrganizer": False,
"isOptional": False,
"status": 1,
"inviterCalendarId": "",
"avatarKey": "",
"category": 3,
"resource": {
"tenantId": meeting.get('tenantId'),
"isDisabled": False
}
}
],
"reminders": [
{
"calendarEventId": "",
"minutes": 5,
"method": 3
}
],
"attachments": [],
"id": "0",
"key": utils.generate_key(),
"originalTime": 0,
"calendarId": "xxx",
"organizerCalendarId": "xxx",
"creatorCalendarId": "xxx",
"successorCalendarId": "",
"originalEventKey": "",
"originalIsAllDay": False,
"isFree": False,
"isAllDay": False,
"isCrossTenant": False,
"source": 1,
"color": -1,
"visibility": 1,
"version": 0,
"rrule": "",
"guestCanInvite": True,
"guestCanSeeOtherGuests": True,
"guestCanModify": False,
"selfAttendeeStatus": 2,
"startTime": startTime,
"startTimezone": "Asia/Shanghai",
"endTime": endTime,
"endTimezone": "Asia/Shanghai",
"summary": "",
"description": "",
"docsDescription": "",
"createTime": 0,
"updateTime": 0
}
}

关于key的生成:

key是服务端用于标识请求数据包的。

1
"key": utils.generate_key()

key的生成在PN()函数的hN()函数中:

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
xk4V: function(t, e, r) {
var n = r("4fRq")
, i = r("I2ZF");
t.exports = function(t, e, r) {
var o = e && r || 0;
"string" == typeof t && (e = "binary" === t ? new Array(16) : null,
t = null);
var s = (t = t || {}).random || (t.rng || n)();
if (s[6] = 15 & s[6] | 64,
s[8] = 63 & s[8] | 128,
e)
for (var a = 0; a < 16; ++a)
e[o + a] = s[a];
return e || i(s)
}
},

其中n和i都是函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
I2ZF: function(t) {
for (var e = [], r = 0; r < 256; ++r)
e[r] = (r + 256).toString(16).substr(1);
t.exports = function(t, r) {
var n = r || 0
, i = e;
return [i[t[n++]], i[t[n++]], i[t[n++]], i[t[n++]], "-", i[t[n++]], i[t[n++]], "-", i[t[n++]], i[t[n++]], "-", i[t[n++]], i[t[n++]], "-", i[t[n++]], i[t[n++]], i[t[n++]], i[t[n++]], i[t[n++]], i[t[n++]]].join("")
}
},
"4fRq": function(t) {
var e = "undefined" != typeof crypto && crypto.getRandomValues && crypto.getRandomValues.bind(crypto) || "undefined" != typeof msCrypto && "function" == typeof window.msCrypto.getRandomValues && msCrypto.getRandomValues.bind(msCrypto);
if (e) {
var r = new Uint8Array(16);
t.exports = function() {
return e(r),
r
}
} else {
var n = new Array(16);
t.exports = function() {
for (var t, e = 0; e < 16; e++)
0 == (3 & e) && (t = 4294967296 * Math.random()),
n[e] = t >>> ((3 & e) << 3) & 255;
return n
}
}
},

转换成python代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import random

def generate_random_array():
t = 0
n = [0] * 16 # 创建一个包含16个元素的数组,初始化为0
for e in range(16):
if e % 4 == 0: # 检查索引是否是4的倍数(在JS中使用的是位操作,但在Python中可以直接用模运算)
t = int(4294967296 * random.random()) # 生成一个新的随机数,类似于JS中的Math.random()
n[e] = (t >> ((e & 3) * 8)) & 255 # 右移t并截取最后8位
return n


def generate_uuid_string(byte_array, start_index=0):
# 初始化16进制数组
hex_array = []
for r in range(256):
hex_array.append("{:02x}".format(r))

n = start_index
return "{}{}{}{}-{}{}-{}{}-{}{}-{}{}{}{}{}{}".format(
hex_array[byte_array[n]], hex_array[byte_array[n+1]],
hex_array[byte_array[n+2]], hex_array[byte_array[n+3]],
hex_array[byte_array[n+4]], hex_array[byte_array[n+5]],
hex_array[byte_array[n+6]], hex_array[byte_array[n+7]],
hex_array[byte_array[n+8]], hex_array[byte_array[n+9]],
hex_array[byte_array[n+10]], hex_array[byte_array[n+11]],
hex_array[byte_array[n+12]], hex_array[byte_array[n+13]],
hex_array[byte_array[n+14]], hex_array[byte_array[n+15]]
)


def generate_key():

# 生成随机数
s = generate_random_array()

# 设置UUID的版本和变体
s[6] = (s[6] & 0x0f) | 0x40 # UUID version
s[8] = (s[8] & 0x3f) | 0x80 # UUID variant

# # 返回格式化的UUID字符串
return generate_uuid_string(s)

总结

关于protobuf协议的分析还是有很多坑点,需要注意。在js代码的分析过程中遇到不懂的可以对应chatGPT,能减少搜索资料和代码逆向的时间。

参考资料

https://protobuf.dev/

Python 爬虫进阶必备 | 由某知识平台延伸的 Protobuf 协议分析学习