聊一聊乐变上传自动化接入

目标 & 背景

最近我司从外部搞了一个项目,技术栈非常杂,客户端业务为 C# + tolua,打包是 python,服务端是 go,后台是 php + js,其中客户端的资源下载接入的乐变

由于项目有众多渠道,平均一个包的包体差不多就有2G,日常发版流程非常辛苦,除了下图的过程外,如果是包更,还需要运营同学手动上传到各大 store,再加上这个项目本身的打包流程设计功底较差,平均一个渠道出包的时间为 1 小时左右

这样就导致最糟情况下,全量发一个版本的时间成本要 1 天,非常难以置信

本片文章会主要围绕乐变的文件上传相关过程碰到的坑,其他部分不会详细介绍,其中主要使用如下开源库

请求加密过程

乐变提供了相关接入过程的 pyhton demo,正常需要发起 login 请求,获取当前账号的 rsa 公钥,但是乐变默认返回的公钥尾部多了一个换行,这样会导致在 C# 中,无论怎么加密都有问题

在使用时,记得 TrimEnd('\n')

-----BEGIN PUBLIC KEY-----
xxx
END PUBLIC KEY-----

接着以 uploadfile.php 接口为例,最开始看到 demo 代码中的 encData 的数据结构,我以为在 data.encdata 中存的是 string 类型,实际上 encData 在调用 rsa_long_encrypt 后,就变成了 string[],在检查 C# 代码到底哪里不一样,着实花了不少时间

encData = {
    "chid": chid,
    "status": "chunksMerge",
    "name": name,
    "part": part,
    "chunks": totalChunks,
    "md5": fileMd5,
    "asynchronous" : 0,#是否是同步合并,1为异步,需轮询合并状态
    "nosdk": '1'#0表示有sdk,1表示没有sdk
}
encData = json.dumps(encData).encode("utf-8")
encData = base64.b64encode(encData)
encData = rsa_long_encrypt(public_key, encData)
data = {
    "chid": chid,
    "encdata": encData
}

def rsa_long_encrypt(public_key, msg, length=53):
    keyPub = RSA.importKey(public_key)
    pubobj = Cipher_pkcs1_v1_5.new(keyPub)
    res = []
    for i in range(0, len(msg), length):
        encryptedStr = pubobj.encrypt(msg[i:i + length])
        encryptedStr = base64.b64encode(encryptedStr).decode('utf-8')
        res.append(encryptedStr)
    return res

加密过程翻译成 C# 后,大概如下,encData 需要先转成 json,然后转为 base64,接着使用 Rsa 进行加密,以 53 的长度分割成 List,并存入 Request encdata 中,在发送时,将 Request 转为 base64 的 json 数据

public interface ILebianRSA
{
    public static virtual List<string> ToRsa(ILebianRSA data)
    {
        var json = JsonConvert.SerializeObject(data);

        return LebianUtils.RsaEncrypt(json.Base64Encrypt());
    }
}

public class LebianRequest<T> where T : ILebianRSA
{
    [JsonProperty("chid")]    public int          ch_id    { get; private set; }
    [JsonProperty("encdata")] public List<string> enc_data { get; private set; }

    public LebianRequest(int ch_id, T data)
    {
        this.ch_id = ch_id;
        
        enc_data = T.ToRsa(data, type);
    }

    public string ToBase64()
    {
        var json  = JsonConvert.SerializeObject(this);
        return json.Base64Encrypt();
    }
}

public static partial class LebianUtils
{
    public static List<string> RsaEncrypt(string base64, int interval = 53)
    {
        var key    = GetKey(); // 获取当前渠道的公钥
        var result = new List<string>();

        for(var i = 0; i < base64.Length; i += interval)
        {
            var sub = base64.Substring(i, Math.Min(interval, base64.Length - i));

            result.Add(sub.RSAEncrypt(key));
        }

        return result;
    }
}

面对这个接入过程,我很难不吐槽一下,为何乐变的后台域名不是用 ssl 进行加密?反而需要自行实现 ssl 的加密过程

文件上传

这里 http 的库之所以选择 RestSharp,很大程度上是为了给上传增加进度条,由于客户端比较大,考虑到网络波动等问题,此处介绍的是分片上传,下图中的 chunk 代表当前上传的是哪一个分段

图中 eta 这么短,是因为 chunk 已经上传了,此图仅是示意

RestRequestAddFile 接口中,提供了 Func getFile 的参数,这样我们做进度条就简单多了,自定义一个 Stream,在读时,通过外部传入的 ProgressTask,上报读了多少 byte

file class ProgressMS : MemoryStream
{
    public ProgressTask? root_progress;
    public ProgressTask? chunk_progress;

    public override async ValueTask<int> ReadAsync(Memory<byte>      buffer,
                                                   CancellationToken cancellationToken = new CancellationToken())
    {
        var result = await base.ReadAsync(buffer, cancellationToken);
        root_progress?.Increment(buffer.Length);
        chunk_progress?.Increment(buffer.Length);
        return result;
    }
}

我在接入 uploadfile.php 接口时,碰到了两个主要问题

  • 为什么二次上传 chunk 依然全量上传
  • 为什么上传后的文件提示文件格式非法

首先说第一个问题,正常我们需要先问当前 chunk 是否需要上传,然后根据返回值选择是否需要 upload,我发现官方提供的 python demo 这个功能是好的,而我写的 C# 却有问题,我花了很多时间对比两者的差异,最后发现如下区别

# 此处为检查 chunk 是否存在
encData = {
    # 略...
    "status":"chunkCheck",
    "chunkIndex":chunk_index
}

# 此处为上传 chunk
encData = {
    # 略...
    "status":"",
    "chunk":chunk_index
}

在乐变的后台封装中,chunk 是否存在、上传以及分片的合并,统一都是走的 uploadfile.php 接口,通过 status 中具体的值进行区分,但是!不同的逻辑使用的结构不一致,同样都是 chunk_index 字段,检查接口要序列化为 chunkIndex,上传时需要序列化为 chunk

这个同样是让我感到困惑的地方

接着我就碰到了第二个问题,为什么上传文件提示文件格式非法?在 RestSharp 中,我使用如下 API 传输一个 chunk

request.AddFile("file", () => ms, file.Name);

在 python demo 中,是这么上传的

fileData = {'file': (filename, file_chunk_data, 'application/octet-stream')}

这就让我非常费解了,大家都是直接把文件名传进去,是什么导致了结果不一致,接着我尝试直接固定文件名为 test.apk,就成功了

// 成功
request.AddFile("file", () => ms, "test.apk");
// 失败
request.AddFile("file", () => ms, "中文test.apk");

这个项目最终出包的文件名是包含中文的,这就可以得出结论,python 和 RestSharp 在面对中文文件时,在处理文件名的过程上存在差异,为了证明这个差异,在 Surge 中直接开启 流量捕获,在请求数据的 RAW 一栏,我们可以清楚的看到 RestSharp 发送的文件名是经过转码的

在 python 中面对中文名的文件上传,会将中文转换为 ..,比如上面的 中文test.apk就会被转义成 ....test.apk,不过好在 RestSharp 也提供了不转义的选项

var options = new FileParameterOptions {DisableFilenameEncoding = true};
request.AddFile("file", () => ms, file.Name, ContentType.Binary, options);

这样最终的文件名会被转义为 ??test.apk,所以这个问题的核心是乐变的后台并没有对中文名的文件上传做 utf8 相关的支持,又或者他们 php 后台的标准和 RestSharp 的标准不一致

最后

这个项目是其他公司积累了很多年的框架,中间据说换了好几波人,很多代码极难维护。这次乐变的自动化 API 接入过程,让我感到非常难受

后面这个项目,有时间我一定要把乐变剔掉