再谈WiFiEventTrigger——数据处理与发送
OnDataReceived
这里首先回顾一下OnDataReceived的使用场景:
在accept接收到一个请求的socket(fd)时调用onDataEvent,内部数据的主要处理函数就是ProcessPackets,而ProcessPackets的两个主要函数一个就是ParsePacketHead用于将char*buf转换为Packet类型的结构体供OnDataReceived使用
然后看向函数onDataReceived()
这里可拆分程两个部分来看一个是MODULE_AUTH_SDK和其他模式,如果是MODULE_AUTH_SDK模式则调用AuthInterfaceOnDataReceived进行处理,其他的则调用DecryptMessage先将数据进行解密再调用OnModuleMessageReceived进行报文的构建和发送
这里需要清楚的是——OnDataReceived是根据packet中的module的不同来进行不同的数据处理和信息返回的,那么module都有哪些种类呢?
——在auth_conn.h中我们可以看到全部的模式:
而第一个要讲述的函数就是指定模式为MODULE_AUTH_SDK调用的处理函数
1. AuthInterfaceOnDataReceived
这里又涉及另一个管理会话的结构体g_authSessionMap——其实质是结构体AuthSession数组:
每个连接每次会话的信息保存在该结构体中
函数流程:
1. 通过函数AuthSessionMapInit检查g_authSessionMap是否存在,不存在则会初始化
2. 调用AuthGetAuthSessionBySeqId通过序列号从数组中找到对应的auth,如果是新连接的第一次数据则没有该auth则调用AuthGetNewAuthSession在其中添加一个auth
3. switch-case语句当模式为MODULE_AUTH_SDK调用AuthProcessReceivedData进行数据的处理
这里使用switch-case语句是方便扩展
1.1 AuthProcessReceivedData
函数比较简单:
- 当g_hcHandle不存在时调用AuthInitHiChain对g_hcHandle进行初始化;初始化失败调用AuthDelAuthSessionBySessionId删除g_authSessionMap中SessionId对应的内容
- 封装request——包含数据和数据大小两项内容
- 调用receive_data——这里没有该函数的具体内容,但是可以推测出该函数调用g_hcHandle中封装的处理函数对request进行处理并发送了回复消息
所以代码的重心落在了最开始的初始化AuthInitHiChain
1.2 AuthInitHiChain 处理函数g_hcHandler
重要的就是hiChainCallback的五个回调函数——其中的AuthConfirmReceiveRequest从函数名可以知道这就是回复请求确认的函数,由于代码的补全这里无法给出g_hcHandle的详细解读,留待读者阅读完善。
然后就是其他模式的通用步骤:主体数据解码——消息封装发送
2. DecryptMessage
先放上代码的注解
/*
函数功能:解密信息(信息分为密文和明文)
函数参数:module:类型;data:报文主体数据;dataLen:数据大小
函数返回:解析过后的cJSON类型的数据
*/
static cJSON *DecryptMessage(int module, const char *data, int dataLen)
{
if (data == NULL) {
return NULL;
}
//通过传入的参数module判断是否为加密密文,不是则调用解密明文函数返回cJSON类型的指针
if (!ModuleUseCipherText(module)) {
return DecryptPlainMessage(data, dataLen);
}
//是密文则先判断数据长度是否正确
if (dataLen < MESSAGE_ENCRYPT_OVER_HEAD_LEN) {
return NULL;
}
//从数据中得到keyIndex
int index = GetKeyIndex(data, 0, MESSAGE_INDEX_LEN);
//根据index从SessionKey链表中获取对应的*sKey
SessionKey *sKey = AuthGetSessionKeyByIndex(index);
if (sKey == NULL) {
SOFTBUS_PRINT("[AUTH] DecryptMessage get session key fail\n");
return NULL;
}
//获取数据的真正长度并分配空间
unsigned int len = dataLen - MESSAGE_ENCRYPT_OVER_HEAD_LEN + 1;
unsigned char *output = malloc(len);
if (output == NULL) {
return NULL;
}
if (memset_s((void *)output, len, 0, len) != EOK) {
free(output);
output = NULL;
return NULL;
}
//构造密钥结构体
AesGcmCipherKey cipherKey = {0};
cipherKey.keybits = GCM_KEY_BITS_LEN_128;
int ret = memcpy_s(cipherKey.key, SESSION_KEY_LENGTH, sKey->key, AUTH_SESSION_KEY_LEN);
ret += memcpy_s(cipherKey.iv, IV_LEN, data + MESSAGE_INDEX_LEN, IV_LEN);
if (ret != 0) {
free(output);
output = NULL;
return NULL;
}
//调用aes_gcm.c文件中的解密函数将解析的结果放入output
ret = DecryptTransData(&cipherKey,
(unsigned char*)&data[MESSAGE_INDEX_LEN], dataLen - MESSAGE_INDEX_LEN, output, len);
if (ret <= 0) {
SOFTBUS_PRINT("[AUTH] DecryptMessage DecryptTransData fail\n");
free(output);
output = NULL;
return NULL;
}
//通过cJSON.c中的函数将其封装为cJSON类型的数据并返回
cJSON *msg = cJSON_Parse((char*)output);
free(output);
output = NULL;
return msg;
}
主要是三个功能部分:解密前的cipherKey的构造——调用DecryptTransData在mbedtls框架下用AES_GCM模式进行解密——调用cJSON_Parse将输出转换为cJSON模式进行返回
这里的代码还是比较好理解的,需要注意三点:
- 在代码的开始首先调用了ModuleUseCipherText来判断当前的模式是否为加密模式
从这里我们可以看到几种不需要加密密文的模式:
#define MODULE_TRUST_ENGINE 1
#define MODULE_HICHAIN 2
#define MODULE_HICHAIN_SYNC 4
#define MODULE_AUTH_CHANNEL 8
#define MODULE_AUTH_MSG 9
所以加密模式应用于:
#define MODULE_CONNECTION 5
#define MODULE_SESSION 6
#define MODULE_SMART_COMM 7
如果是不需要加密的模式则调用DecryptPlainMessage将数据复制一遍即可
-
第二点就是加密参数的准备得到AesGcmCipherKey:
主要是从g_sessionKeyList中根据GetKeyIndex从data出取出的index确定的sKey存入上述结构体的char key[ ]数组中:
char iv[]数组中存入data + MESSAGE_INDEX_LEN, IV_LEN
然后就可以将其传入DecryptTransData调用es_gcm.c文件中的解密函数将解析的结果放入output中(AES对称加密) -
最后调用cJSON_Parse——cJSON一种轻量的用于网络传输的结构体,将output转换为该类msg并返回,则一个报文的解密到此结束!
最后是对报文发送客户端的相应代码
3. OnModuleMessageReceived
代码比较简洁,主要处理两类模式的的消息应答:
- MODULE_TRUST_ENGINE:
- MODULE_CONNECTION:正常的TCP连接的通信
3.1 OnMsgOpenChannelReq
函数流程:
1. 调用MsgGetDeviceIdUnPack从msg中解析出cmd,deviceId,authId存入conn
2. 调用BusGetLocalDeviceInfo获取当前设备的全局存储变量g_deviceInfo
3. 调用MsgGetDeviceIdPack将本地设备的CMD_TAG,deviceId存入reply
4. 调用AuthConnPostMessage进行信息的封装和发送
这里需要注意两点:
- 调用MsgGetDeviceIdUnPack解析出来的deviceid和MsgGetDeviceIdPack存入reply的deviceId并不一样,一个是报文发送客户端的deviceId,一个是服务端本地存储的设备id也就是本地设备的deviceId,后面存入reply的deviceId是为发送回应报文需要的本地设备deviceId
- 最后调用的AuthConnPostMessage信息推送函数在下面的OnMessageReceived中也使用到(所以在下面会详细提到),但是传入的模型参数不同,注意区分
3.2 OnMessageReceived
函数简单的做了两个工作:首先调用cJSON_GetObjectItem从msg中出去CODE对应的值;然后根据CODE的值选择两个不同的Verify函数进行后续处理,一个VerifyIp,另一个VerifyDeviceId
这里需要区分一下这两个函数的区别
3.2.1 OnVerifyIp
同样可以分为三步走:
- MsgVerifyIpUnPack:对于拆封的msg进行验证:更新支持的总线版本(minVersion&maxVersion);比对本地的deviceName和Type与msg中的是否一致;验证msg中是否存在AUTH_PORT和SESSION_PORT;上述的验证都通过才能进行下一步,否则则视为报文发送不正确,直接return
- MsgVerifyIpPack:上述验证正确后,需要重新组织发送回应的报文,以cJSON的格式组织,里面包含的内容如下:
报文内容:
CODE:CODE_VERIFY_IP
BUS_MAX_VERSION:connInfo->maxVersion
BUS_MIN_VERSION:connInfo->minVersion
AUTH_PORT:authPort
SESSION_PORT:sessionPort
CONN_CAP:DEVICE_CONN_CAP_WIFI
DEVICE_NAME:devInfo->deviceName
DEVICE_TYPE:DEVICE_TYPE_DEFAULT
DEVICE_ID:devInfo->deviceId
VERSION_TYPE:devInfo->version
- AuthConnPostMessage:将构造的报文reply以及模式等信息传入进行后续操作
3.2.2 OnVerifyDeviceId
相较于OnVerifyIp这里的验证和封装就显得简单的多
- GetJsonString:从request中获取deviceId
- MsgVerifyDeviceIdPack:封装reply:
报文的内容:
reply:{
CODE:CODE_VERIFY_DEVID
DEVICE_ID:info->deviceId
}
- 调用AuthConnPostMessage进行传输
可以看到与OnVerifyIp相比,封装的信息十分有限,可以看成是它的一个简略版本(deviceId在OnVerifyIp中也有封装)
最后是发送函数
3.2.3 AuthConnPostMessage
函数实现两个功能:调用cJSON_PrintUnformatted将传入的cJSON格式的msg转换为char*;调用AuthConnPostBytes进行传输
3.2.4 AuthConnPostBytes
函数实现两个功能:首先是对packet进行打包——进行加密;然后调用AuthConnSend进行传输
3.2.4.1 AuthConnPackBytes
/*
函数功能:完成TCP中packet的打包以及发送
函数参数:module:模式;flags:FLAGS_REPLY;seqNum:报文序列号;str:上层封装的用于传输的信息;bufLen:0
函数返回:-1:发送失败;0:发送成功
*/
unsigned char* AuthConnPackBytes(int module, int flags, long long seqNum, const char *str, int *bufLen)
{
if ((str == NULL) || (bufLen == NULL)) {
return NULL;
}
bool isCipherText = ModuleUseCipherText(module);//调用上述模块函数,得知是哪种加密手段
int dataLen = isCipherText ? (strlen(str) + MESSAGE_ENCRYPT_OVER_HEAD_LEN) : strlen(str);//三目运算符,是否为加密信息,是则需要额外的头来存储信息
int len = dataLen + PACKET_HEAD_SIZE;//长度等于数据长度加头部标识长度
unsigned char *buf = (unsigned char *)calloc(1, sizeof(unsigned char) * len);
//在内存的动态存储区中分配num个长度为size的连续空间,函数返回一个指向分配起始地址的指针;如果分配不成功,返回NULL
if (buf == NULL) {
return NULL;
}
//将所有参数的信息使用安全拷贝,拷贝到data中
unsigned char *data = buf;
int identifier = PKG_HEADER_IDENTIFIER;
unsigned int ret = (unsigned int)memcpy_s(data, sizeof(int), &identifier, sizeof(int));
data += sizeof(int);
ret |= (unsigned int)memcpy_s(data, sizeof(int), &module, sizeof(int));
data += sizeof(int);
ret |= (unsigned int)memcpy_s(data, sizeof(long long), &seqNum, sizeof(long long));
data += sizeof(long long);
ret |= (unsigned int)memcpy_s(data, sizeof(int), &flags, sizeof(int));
data += sizeof(int);
ret |= (unsigned int)memcpy_s(data, sizeof(int), &dataLen, sizeof(int));
data += sizeof(int);
if (ret != 0) {
free(buf);
return NULL;
}
//如果是密文
if (isCipherText) {
//需要获取密钥并将主体数据进行加密
if (GetEncryptTransData(seqNum, str, data, dataLen, bufLen) != 0) {
free(buf);
return NULL;
}
//否则直接拷贝入data即可
} else {
if (memcpy_s(data, dataLen, str, dataLen) != EOK) {
free(buf);
return NULL;
}
*bufLen = len;
}
return buf;//返回实际数据
}
函数流程:
1. 调用ModuleUseCipherText判断当前模式是否为需要加密的模式
2. 将传输相关头部信息添加入data中包括:
PKG_HEADER_IDENTIFIER
module
seqNum
flags
dataLen
3. 如果需要加密则调用GetEncryptTransData进行加密(加密的具体步骤不详细讲解,这里需要注意的是在构造密钥结构体中的iv时,采用了randomIv+seqNum的方法),将结果存入data中,不需要加密则直接将data拷贝入data中,最后返回buf
3.2.4.2 AuthConnSend
调用TcpSendData——调用send操作将buf发送到fd对应的客户端
至此数据处理部分全部完结!
4. 数据处理流程图
最后给出整个数据处理流程由于调用十分深,涉及的函数十分庞杂,所以流程图简化了许多,仅供读者参考,更好的理解其中的调用和流程流转