8

fastlane spaceship 源码浅析

 3 years ago
source link: http://saitjr.com/ios/2020-01-07-fastlane-spaceship.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

提到 iOS 持续集成,fastlane 应该是绕不过的一个工具,翻阅了一下网上大部分的资料,基本上是用了都说好,然后详谈如何调用。用到的功能,常见是 gymmatchdeliver 。在我看来,fastlane 最强大的地方,不在于怎么方便了打包,毕竟不依赖 gym ,直接用 xcodebuild 问题也不大,关键在于它所提供的思路。

环境信息

fastlane: 2.140.0

非常亮眼的地方主要有三个:

  • match ,提供了一种全新的证书管理的方式,这个流程在之前 fastlane match 源码浅析与最佳实践 中介绍过。
  • spaceship ,封装并且优化了访问苹果账号的各个步骤。
  • workflow,如果写过 Fastfile,那么一定知道流程的封装是用的 lane 。所以 Fastfile 也提供了一种工作流的模式。之前的两篇文章,可能对理解 lane 是什么以及实现方式有帮助: Rake task vs. Fastlane lanefastlane DSL 源码浅析

除此之外,fastlane 还打通了各个社交平台,比如 twitter、slack,由于国内(Emmm…),再加上每个公司所用的管理平台不一样,所以,这个功能倒显得比较鸡肋。

Spaceship 是什么

也许你会好奇,难道 fastlane 在你眼里亮点就这几个? gym 不是,那 deliver 总是吧,那 pilot 总是吧。当然,这些 action 也很好用,但是他们都有一个必不可少的步骤,就是访问苹果账号,除此之外, match 也要用到。只要和苹果账号相关,就绕不开 spaceship 。扯了这么多, spaceship 到底是什么?

来看看 官方介绍

Spaceship is a Ruby library that exposes the Apple Developer Center and iTunes Connect API. It’s super fast, well tested and supports all of the operations you can do via the browser. Scripting your Developer Center workflow has never been easier!

也就是说,Spaceship 封装了你要访问苹果的所有 API。在换句话说,你想做,但是 fastlane 没做的事情,都可以通过单独调用 spaceship,自己实现,这也是我读 spaceship 源码的初衷。

2018.06.09 更新。这篇文章刚开头,可能就要凉。WWDC2018 苹果官方推出了 AppStore API,爸爸你是不是见不得别人做得好。
2018.06.10 更新。认真看完 Session 303,发现 spaceship 凉不了,反而会更加强大。苹果 API 给 spaceship 提供了更快的访问支持,用于替代现有的 web scraping。而对于用户来说,直接访问苹果 API 还是很麻烦的,毕竟要管理 token 什么的,所以 spaceship 成为最强非官方 App Store Connect API in Ruby 应该是指日可待了。
2020.01.07 更新。啊,这篇文章鸽了一年半了,一直没更新。中间还被人喷过,太不容易了,01.11 前完成。

会关注哪些

由于 Spaceship 细节较多,关于 API 如何调用相关的问题,建议直接看 API 文档(看文档更新时间在 2019.2,不确定是否有变动;2018 WWDC 的时候,苹果说将 developer 和 itunesconnect 整合为 applestoreconnect,但接口上还是有区别的,fastlane 中还是分开的,所以还是用原来的称呼):

  • Developer Portal APIdeveloper.apple.com 相关的接口,开发者网站,一般是和证书相关的
    • AppID 注册
    • 证书、描述文件、设备
    • 开发者管理
  • App Store Connect APIitunesconnect.apple.com ,和 App 上架相关的
    • App 列表、信息
    • TestFilght / AppStore
    • 测试用户、App 权限管理

除了 API 以外,对苹果的两步验证、Spaceship 设计、如何保证多个账号是登录态、如何抓取苹果接口相关内容比较感兴趣,源码也会按照这个目标去看。

源码浅析

登录验证

2SV vs 2FA

在登录 AppleID 的时候,通常会用另一台设备来确认登录,还要输入一个 code。这个环节就是登录验证的一部分。相比用 Password 登录的情况,2SV 与 2FA 注重不是“你知道什么”,而是“你拥有什么”。也就是借助其他设备(or 账号/密码等)来验证身份,两者都是防止密码泄露的情况下,登录 AppleID。

  • 2SV (Two-step verification) ,即「两步验证」。是针对早期设备/系统设计的验证方式,需要注册一部受信设备与电话号码,在登录时,通过发送 4 位数验证码 进行验证。
  • 2FA (Two-factor authentication) ,即「双重认证」。在 iOS9 与 OS X El Capitan 以后,登录时,需要填写受信设备上的 6 位验证码 ,在设置中可以选择用短信还是语音进行接收。

2SV 是比较旧的验证方式,2FA 的登录方式更流畅。在 Spaceship 的 Spaceship::Client.handle_two_step_or_factor 中,包含了这两种登录流程。

获取验证方式

通过 https://idmsa.apple.com/appleauth/auth 接口,可以判断:

if r.body["trustedDevices"].kind_of?(Array)
  handle_two_step(r) # 2SV 两步验证
end
if r.body["trustedPhoneNumbers"].first.kind_of?(Hash)
  handle_two_factor(r) # 2FA 双重认证
end

除此之外,返回值中还包含账号是否被锁、受信设备列表等信息。

2SV 发送验证码

发送 PUT https://idmsa.apple.com/appleauth/auth/verify/device/#{device_id}/securitycode 请求,Apple 将向用户指定设备发送验证码,同时 Fastlane 命令行会提醒用户输入。随后再 POST 该接口,带上验证码,通过验证。

2FV 发送验证码

2FV 的手机号可以通过环境变量 SPACESHIP_2FA_SMS_DEFAULT_PHONE_NUMBER 默认指定,也可以在命令行中自己选择。POST https://idmsa.apple.com/appleauth/auth/verify/phone 并带上 phone_id 等信息,触发验证码发送。然后再执行验证码验证,验证码输入错误,会自动重试以上流程,知道用户输入正确。

可以看到,Spaceship 并没有用到 Apple 官方的 AppStoreConnect API,还是保持着原来的抓包的方式。AppStoreConnect 尝试过好几次,先不说 ProvisionProfile 那部分的 API 在 WWDC 开完一年多以后才出来,就是 App 的信息获取,都没有 Spaceship 的全面。另一点还有 AppStoreConnect API 的 AppKey,也不是很好用。

登录态

在双重认证没问题以后,就算是登录成功了,下一步需要关注的是如何保存登录态。尤其是苹果在 2017.12.04 新增了登录频控以后,即使登录成功,也会进行计数。所以,无论是出于优化 Fastlane 执行速度,还是避免触发频控的角度,登录态的 CRUD 操作都是非常有必要的。

如何确认当前 Session 有效

Spaceship::Client.fetch_olympus_session 方法中,通过 GET https://appstoreconnect.apple.com/olympus/v1/session 来获取用户信息。在 Session 有效的情况下,能获取到当前登录用户信息(可以尝试在登录/未登录的情况,在浏览器中访问这个地址)。

未登录:

Unauthenticated
Request ID: YMGYJRRRWUB4NX5G64GXFDYT.0.0

登录(部分字段):

{
  "provider": {
    "providerId": 123,
    "name": "Jiarong Tang",
    "contentTypes": [
      "SOFTWARE"
    ],
    "subType": "INDIVIDUAL"
  }
}

登录态的读取与更新

在登录前后,都会对 Cookie 进行读取,存储地址不出意外的话应该是 ~/.fastlane/spaceship/#{AccountName}/cookie ,也可能在其他目录下,可以在 Spaceship::Client.persistent_cookie_path 方法中找到。

Fastlane 网络请求用的是 faraday ,Cookie 管理用的 faraday-cookie_jar ,其中封装了 Cookie 的 CRUD 操作(粗暴的理解为文件 CRUD 也行)。Cookie 文件为:

- !ruby/object:HTTP::Cookie
  name: DES5e2acab1152418c652xxx
  value: HSARMxxx+xxx/DcMreoGP6UzhYqWMEhxxxx/JhSRVX
  domain: idmsa.apple.com
  for_domain: true
  path: "/"
  secure: true
  httponly: true
  expires: 
  max_age: 2592000
  created_at: &1 2020-01-13 09:26:01.659268000 +08:00
  accessed_at: *1

当然,还有一些其他的 Cookie,需要的 Cookie 是 name.start_with?('DEC') || name == 'myacinfo'

登录流程

登录逻辑封装在 Spaceship::Client.send_shared_login_request 中。在调用到这个方法之前, portal_clienttunes_client 有一些登录逻辑上的差异需要处理。以下是 send_shared_login_request 这部分通用逻辑:

  1. 尝试读取 Cookie 文件。文件存在则判断 Session 是否有效,有效直接返回;
  2. 判断是否有通过环境变量 FASTLANE_SESSIONSPACESHIP_SESSION 传入的 Cookie 文件,并判断是否有效,有效直接返回;
  3. 尝试查找 DES 的 Cookie,通过 https://idmsa.apple.com/appleauth/auth/signin 执行登录;
  4. 如果返回 409 ,说明有双重认证,则执行认证逻辑;
  5. 认证完成,登录成功,请求 https://idmsa.apple.com/appleauth/auth/2sv/trust 接口,更新本地 Cookie;
  6. 存储 Cookie,登录流程结束。

Spaceship 结构

fasttlane/spaceship/lib/spaceship 中,可以发现 tunestest_flightportalconnect_api 四个文件夹。一眼看去,除了 portaltunes 比较明确含义以外,剩下两个感觉有点重复。以及除了这四个文件夹,还有其他文件。

connect_api

connect_api 文件夹中,有 modelsprovisioningtestflightusers 几个文件夹。根据文件夹名字以及 connect_api 的名字,猜测这里面应该对官方 AppStoreConnect API 的封装。

connect_api/client.rb 中, host https://api.appstoreconnect.apple.com/v1/ ,即官方 API 地址。所以 connect_api 应该是正在切到 AppStoreConnect API 的调用。全局搜了一下,目前 pilot 已经用上了。

然后再来看调用。在 connect_api/models 中封装了很多模型,比如 app.rb 中获取全部 app 数组:

def self.all(filter: {}, includes: nil, limit: nil, sort: nil)
  resps = Spaceship::ConnectAPI.get_apps(filter: filter, includes: includes, limit: limit, sort: sort).all_pages
  return resps.flat_map(&:to_models)
end

理想情况下,应该是调用 Spaceship::ConnectAPI::TestFlight.get_apps ,但是这里省去了 TestFlight ,并且看不到任何登录操作。怀疑是用 extend 扩展了方法。全局搜索 extend Spaceship 发现在 spaceship/lib/spaceship 中(也就是 connect_api 同级目录下)还有一个 connect_api.rb 文件:

module Spaceship
  class ConnectAPI
    extend Spaceship::ConnectAPI::Provisioning
    extend Spaceship::ConnectAPI::TestFlight
    extend Spaceship::ConnectAPI::Users
  end
end

所以这里才是 ConnectAPI 的入口(之前看漏了)。登录在 Spaceship::ConnectAPI::TestFlight::Client.instance 中,通过判断 Spaceship::ConnectAPI.token 来确定是否支持 ConnectAPI 的调用方式,如果不支持,则降级为 Tunes::Client 的初始化方式。

test_flight / tunes / portal

剩下的三个 client 都差不多,均继承自 Spaceship::Client (ConnectAPI 的基类也是这个,但 ConnectAPI 重写了 initliaze )。也就是说,登录验证流程都相同。区别在于, hostname 不同,以及职责不同。

其他文件

以上主要是各平台相关的,除此之外,spaceship 下还有:

  • du: 用于上传附件,比如 App 截图、icon 等
  • command_generator.rb: 这个文件在 fastlane DSL 源码浅析 这篇博客中介绍过,用于解析命令行参数。别忘了 spaceship 也是一个普通的 action,可以命令行调用的,参数查看可以用 fastlane action spaceship
  • launcher.rb: launcher 维护了多个 client 实例,可以登录多个账号。

最后

Spaceship 的源码阅读到这里就告一段落了,中间由于工作变更鸽了一年多。关于应用场景,之前写了 苹果账号证书体系自动化策略 欢迎讨论。

connect_api 的运用来看,可以发现苹果这部分 API 仍然很不完善,Fastlane 还在逐步切换当中。如果在程序中直接调用 Spaceship,可能需要注意 API 的变化(大概率会向下兼容)。如果后端技术栈不是 Ruby,调用 Fastlane 有一定难度,可以根据 Spaceship 登录鉴权,以及抓到的包来重写。所以这次源码阅读还是很有意义的。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK