# 聊一聊乐变上传自动化接入 ## 目标 & 背景 最近我司从外部搞了一个项目,技术栈非常杂,客户端业务为 C# + tolua,打包是 python,服务端是 go,后台是 php + js,其中客户端的资源下载接入的乐变 由于项目有众多渠道,平均一个包的包体差不多就有2G,日常发版流程非常辛苦,除了下图的过程外,如果是包更,还需要运营同学手动上传到各大 store,再加上这个项目本身的打包流程设计功底较差,平均一个渠道出包的时间为 1 小时左右 这样就导致最糟情况下,全量发一个版本的时间成本要 1 天,非常难以置信 ![](https://blog.liuocean.synology.me:9001/blog/old/17057147615938.jpg) 本片文章会主要围绕乐变的文件上传相关过程碰到的坑,其他部分不会详细介绍,其中主要使用如下开源库 - [CliToolkit](https://github.com/LiuOcean/CliToolkit) 之前开源的 Cli 框架 - [RestSharp](https://github.com/restsharp/RestSharp) http 客户端 - [Masuit.Tools](https://github.com/ldqk/Masuit.Tools) 处理 rsa 等加密内容 ## 请求加密过程 乐变提供了相关接入过程的 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# 代码到底哪里不一样,着实花了不少时间 ```python 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 数据 ```csharp public interface ILebianRSA { public static virtual List ToRsa(ILebianRSA data) { var json = JsonConvert.SerializeObject(data); return LebianUtils.RsaEncrypt(json.Base64Encrypt()); } } public class LebianRequest where T : ILebianRSA { [JsonProperty("chid")] public int ch_id { get; private set; } [JsonProperty("encdata")] public List 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 RsaEncrypt(string base64, int interval = 53) { var key = GetKey(); // 获取当前渠道的公钥 var result = new List(); 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 已经上传了,此图仅是示意 ![](https://blog.liuocean.synology.me:9001/blog/old/17057197639543.jpg) 在 **RestRequest** 的 **AddFile** 接口中,提供了 **Func getFile** 的参数,这样我们做进度条就简单多了,自定义一个 Stream,在读时,通过外部传入的 ProgressTask,上报读了多少 byte ```csharp file class ProgressMS : MemoryStream { public ProgressTask? root_progress; public ProgressTask? chunk_progress; public override async ValueTask ReadAsync(Memory 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# 却有问题,我花了很多时间对比两者的差异,最后发现如下区别 ```python # 此处为检查 chunk 是否存在 encData = { # 略... "status":"chunkCheck", "chunkIndex":chunk_index } # 此处为上传 chunk encData = { # 略... "status":"", "chunk":chunk_index } ``` 在乐变的后台封装中,chunk 是否存在、上传以及分片的合并,统一都是走的 **uploadfile.php** 接口,通过 **status** 中具体的值进行区分,但是!不同的逻辑使用的结构不一致,同样都是 chunk_index 字段,检查接口要序列化为 **chunkIndex**,上传时需要序列化为 **chunk** > 这个同样是让我感到困惑的地方 接着我就碰到了第二个问题,为什么上传文件提示文件格式非法?在 RestSharp 中,我使用如下 API 传输一个 chunk ```csharp request.AddFile("file", () => ms, file.Name); ``` 在 python demo 中,是这么上传的 ```python fileData = {'file': (filename, file_chunk_data, 'application/octet-stream')} ``` 这就让我非常费解了,大家都是直接把文件名传进去,是什么导致了结果不一致,接着我尝试直接固定文件名为 **test.apk**,就成功了 ```csharp // 成功 request.AddFile("file", () => ms, "test.apk"); // 失败 request.AddFile("file", () => ms, "中文test.apk"); ``` 这个项目最终出包的文件名是包含中文的,这就可以得出结论,python 和 RestSharp 在面对中文文件时,在处理文件名的过程上存在差异,为了证明这个差异,在 Surge 中直接开启 **流量捕获**,在请求数据的 RAW 一栏,我们可以清楚的看到 RestSharp 发送的文件名是经过转码的 ![](https://blog.liuocean.synology.me:9001/blog/old/17057219427050.jpg) 在 python 中面对中文名的文件上传,会将中文转换为 **..**,比如上面的 **中文test.apk**就会被转义成 **....test.apk**,不过好在 RestSharp 也提供了不转义的选项 ```csharp var options = new FileParameterOptions {DisableFilenameEncoding = true}; request.AddFile("file", () => ms, file.Name, ContentType.Binary, options); ``` 这样最终的文件名会被转义为 **??test.apk**,所以这个问题的核心是乐变的后台并没有对中文名的文件上传做 utf8 相关的支持,又或者他们 php 后台的标准和 RestSharp 的标准不一致 ## 最后 这个项目是其他公司积累了很多年的框架,中间据说换了好几波人,很多代码极难维护。这次乐变的自动化 API 接入过程,让我感到非常难受 后面这个项目,有时间我一定要把乐变剔掉