iOS 企业证书过期填坑记

in #cn-programming7 years ago

入坑篇

前线客服传来消息 — “用户反馈一打开我们的 App,就直接闪退了”,刚听到这个消息,我很吃惊,上一期发的新版本 QA 都有验证过。难道是因为功能权限的问题导致的,赶紧跟客服确认具体情况。原来是客户前几天都能正常使用 App,今天一打开就莫名闪退了。刚了解清楚具体情况,一下子闪退的消息,就如滔滔江水一涌而来,随后也就开始了 iOS 证书过期填坑之旅。

我们公司的产品有几十个客户,但并不是每一家客户都有反馈,而只是其中的几家。反馈闪退的几家客户中,都是同时使用 Android 和 iOS 两个平台,但反馈闪退问题的都是使用 iOS 平台的用户,Android 平台并没有出现闪退问题。

梳理完思路后,我们就想到了是不是反馈闪退的客户使用的 App 证书或描述文件出问题了,因此立马登陆苹果开发者后台,登陆后发现果然是几个客户使用的证书,今天就过期了。那是不是证书过期导致闪退的呢?原生开发人员,马上更新一下证书,打了个包进行验证。果然,用新的证书打出来的包,就能正常使用,不会出现闪退了。网上找了相关的资料,也很多小伙伴遇到同样的问题 —— “企业版证书过期,App 出现闪退”。问题是已经定位了,但客户那边怎么解决呢?客户一打开我们的 App 就立马闪退了,没有办法进行强制更新。此后,在网上绕了一大圈,看了很多文章,发现我们最终的方案,只能重新打包让用户重装。

苍天啊!大地啊!为什么苹果企业证书即将过期,没有发邮件通知,这真是一个大坑!!!事情竟然已经发生,只能咽下苦水,乖乖地接受外部的 “轰炸” 了。接下来我们立即针对闪退的客户重新打包,然后让公司客服与客户沟通,说明情况...

这个问题以后要如何避免?难道要安排专人,每天定时检查证书的有效性?最初的这个想法,其实我是拒绝的。这种脏活累活,肯定要请我们吃饭的家伙 —— 💻(Computer)来帮我们处理咯。前阵子刚好偶遇谷歌出品的一个神器 —— GoogleChrome/puppeteer (Headless Chrome Node API),接下来我们就先来介绍这款神器。

跳坑篇

puppeteer 简介

puppeteer 是一个 Node.js 的库,支持调用 Chrome 的 API 来操纵 Web,相比较 Selenium 或是 PhantomJS,它最大的特点就是它的操作 DOM 可以完全在内存中进行模拟既在 V8 引擎中处理而不打开浏览器,而且关键是这个是 Chrome 团队在维护,会拥有更好的兼容性和前景。

puppeteer 的神技:

  • 对网页进行截图保存为图片或 pdf
  • 抓取单页应用 (SPA) 执行并渲染(解决传统 HTTP 爬虫抓取单页应用难以处理异步请求的问题)
  • 做表单的自动提交、UI的自动化测试、模拟键盘输入等
  • 用浏览器自带的一些调试工具和性能分析工具帮助我们分析问题
  • 在最新的无头浏览器环境里做测试、使用最新浏览器特性
  • 写爬虫做你想做的事情

是不是感觉 puppeteer 棒棒哒。其实还有其它一些无头浏览器,比如:

  • phantomjs:Scriptable Headless WebKit 【Star - 24218】
  • slimerjs:A scriptable browser like PhantomJS, based on Firefox 【Star - 2396】
  • Splash:Lightweight, scriptable browser as a service with an HTTP API 【1617】
  • trifleJS:Headless automation for Internet Explorer 【Star - 740】

简单介绍完 puppeteer,接下来我们就来稍微介绍一下思路。其实实现思路很简单,只需要使用 puppeteer 模拟登录🍎开发者网站,进入证书管理的页面,获取所有证书的有效期,然后设置计算出即将过期的天数。

最终的流程如下:

  • 访问🍎开发者官网
  • 进入证书管理页面,获取指定类型证书(All、Pending、Development 或 Production)
  • 取得证书列表,以当天的时间点为每个证书计算即将过期的天数
  • 基于处理完的数据,进行预警通知(邮件、短信或微信)

puppeteer 实战

基于 puppeteer API 的版本为:0.11.0

const puppeteer = require('puppeteer');

(async() => {
    const browser = await puppeteer.launch({
        headless: false // 开发调试阶段,设置为false
    });
    const page = await browser.newPage();
    page.setViewport({
        width: 1376,
        height: 768,
    });
    page.on('response', async(response) => {
        if (response && response.status == 200) {
            // 判断是否加载完概览视图,然后再次进入证书页面
            if (response.url.indexOf('tpl.overview-view.html') != -1) {
                await getCertsInfo();
            }
            // 判断是否为生产环境证书列表请求
            if (response.url.indexOf('status=4&certificateStatus=0&type=distribution') 
                != -1) {
                const res = await response.json();
                if (res && res.certRequests) {
                    console.dir(calcCertsDays(res.certRequests));
                    await browser.close();
                }
            }
        }
    });

    // 跳转到苹果官网并等待页面资源加载完成
    await page.goto('https://developer.apple.com/cn/', {
        waitUntil: 'load'
    });

    // 跳转到登录页面
    await page.click('.ac-gn-account > a');
    await page.waitForSelector('#accountname', {
        timeout: 50000
    });
    await login();

    // 执行登录操作
    async function login() {
        await page.focus('#accountname');
        await page.type('开发者账号', { // 此处替换为真实账号
            delay: 100
        });
        await page.focus('#accountpassword');
        await page.type('开发者账号密码', { // 此处替换为真实密码
            delay: 100
        });
        await page.click('#submitButton2');
    }

    // 获取证书信息(等待模板加载完成后,才进入证书管理页面)
    async function getCertsInfo() {
        const CERT_ITEM_SELECTOR = '#main section.getting-started > a:nth-child(2)';
        await page.waitForSelector(CERT_ITEM_SELECTOR);
        await page.click(CERT_ITEM_SELECTOR);
        const PROD_CERT_SELECTOR = 'li.subitem > a[href*="certificate/distribution"]';
        await page.waitForSelector(PROD_CERT_SELECTOR);
        await page.click(PROD_CERT_SELECTOR);
        await page.waitForSelector(PROD_CERT_SELECTOR);
    }

    // 计算每个证书的天数
    function calcCertsDays(certs) {
        if (Array.isArray(certs)) {
            const today = new Date();
            return certs.map(cert => {
                return {
                    name: cert.name,
                    type: cert.typeString,// Apple Push Services || iOS Distribution
                    expirationDay: dateDiff(today, new Date(cert.expirationDate))
                };
            });
        }
    }

    // 计算两个日期的间隔天数
    function dateDiff(today, expirationDate) {
        return parseInt((Math.abs(expirationDate.getTime() - today.getTime())) 
          / 1000 / 60 / 60 / 24);
    }
})();

总结

通过 puppeteer 这款神器提供强大的 API,我们只是实现了基本功能,后面还有一些功能需要优化和开发,比如异常处理、账号信息灵活配置和预警机制等,目前初步打算优先使用 wechaty 微信个人号🤖开发框架,来实现消息通知。个人感觉 puppeteer 在日后的工作中,还有很多用武之地,比如此前个人使用 puppeteer 实现了简单的自动化测试功能。

此外在填坑过程中,偶遇了另一款神器 —— fastlane (The easiest way to automate building and releasing your iOS and Android apps) ,感觉真是相见恨晚啊(前阵子部门刚花大力气,实现App自动打包)。有兴趣的小伙伴,可以试试 puppeteerfastlane 这两款神器。

Coin Marketplace

STEEM 0.20
TRX 0.13
JST 0.030
BTC 64916.21
ETH 3483.89
USDT 1.00
SBD 2.45