Keep APP技术研究

2020-10-17 09:31:17 浏览数 (1)

最近在研究运动软件Keep,就是那个自律给我自由的Keep。主要方法是使用Charles来抓包,然后查看接口。由于Charles是一款Mac的应用,所以Windows系统,可能不能实践了。另外安卓手机限制不能抓包HTTPS的协议,所以也不能实践了。

现在分享一下我的研究成果,本文可能触及到Keep软件的一些特殊操作,大家谨慎使用,本文仅供学习和交流使用,如果侵犯到Keep的相关利益,请联系我。


Keep是典型的混合式开发,也就是前端H5 后端 移动端(安卓和iOS),大多界面都是使用了前端技术开发的,主要前端框架是基于VUE来做的。

主要域名

后端服务域名:https://api.gotokeep.com 主站H5:https://show.gotokeep.com 活动等H5:https://m.gotokeep.com 静态资源CDN:https://static1.keepcdn.com 监控:https://apm.gotokeep.com 智能设备:https://kit.gotokeep.com

userAgent

userAgent是混合开发中,H5用来识别APP内部与外部的重要依据。前端可以通过JavaScript代码window.navigator.userAgent来获取,Keep的userAgent`如下:

Mozilla/5.0 (iPhone; CPU iPhone OS 13_5_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148;Keep/6.43.0 (iPhone; iOS 13.5.1; Scale/2.00);Keep/6.43.0 (iPhone; iOS 13.5.1; Scale/2.00)

当然不同系统是不一样的,其中最重要的是最后Keep/版本号(其他信息)这一段,至于为什么要写2遍,我也不清楚,难道客户端植入的时候多写了一遍?

调试页面

Keep线上的页面都是线上环境,调试线上环境的其实也没有多大的意义。由于我们拿不到Keep的源代码,所以只能通过线上代码简单地看看Keep页面的结构。

Keep使用了vue vue-router vuex这样的框架组合,它的页面链接的最后一级是用户的userId,我们以“我的等级”页面为例,如:https://show.gotokeep.com/experience/grades/xxxxxxxxxxxxxxxxxxxxxxxx?kg=16其中xxxxxxxxxxxxxxxxxxxxxxxx就是userId,由于用户的userId是隐私数据,所以我就那x来代替了(下面所有有userId的地方,我都会用xxxxxxxxxxxxxxxxxxxxxxxx来代替)。

浏览器直接打开这个页面,发现报错了,仔细一看,你会发现是接口https://api.gotokeep.com/diamond/v1/users/xxxxxxxxxxxxxxxxxxxxxxxx/privilegeWall/levels返回了401,接口401说明没有授权。为了让页面实现在Keep中同样的效果,可以做下面的几步:

  1. 让H5页面识别浏览器为Keep站内。 要识别站内就是使用上面的userAgent,打开开发者工具,然后选择Network conditions面板,去掉Select automatically的勾选,然后把上面的userAgent粘贴到下面的输入框中,如下,然后刷新一下userAgent就生效了。
  1. 获取cookie。 Keep接口认证是基于JWT来实现的。我们使用Charles来查看任一接口的cookie,会发现有一个authorization的字段,这个就是JWT的关键,如下:
  1. 设置cookie。 为了页面能正常运行,我们把所有的cookie信息,都设置进去。打开浏览器的开发者工具,然后依次把cookie粘贴进去,如下:

此时刷新页面,可以看到页面已经可以正常运行了,如下:

客户端事件

客户端事件是H5和客户端(这里只有移动端)交互的指令,其实就是一个特定协议的字符串,前端使用location.href = 客户端事件字符串来执行客户端事件,在Keep中为了方便调试,也可以扫码来执行这些事件。举个例子,打开webview的事件是keep://webview/后面跟着encode的URI就可以实现跳转页面了,比如要使用Keep来跳转本博客,就可以如下:keep://webview/https://www.kai666666.top/,你可以使用这个工具来encode,把刚才的事件,转换为二维码,如下:

现在打开Keep扫一扫,上面的二维码,你就可以用Keep进入本博客网站了。要把字符串转换为二维码可以使用草料二维码。

https://api.gotokeep.com/config/v2/basic?refresh=true接口中定义了更多的事件:

描述

事件

精选

keep://discover_web

训练

keep://discover_course

饮食

keep://discover_food

商城

keep://discover_store

精选

keep://discovery/explore

训练

keep://discovery/course

攻略

keep://discovery/guide

饮食

keep://discovery/diet

商城

keep://discovery/product

推荐

keep://hottabs/hot

热门视频

keep://hottabs/video

运动时刻

keep://hottabs/story

training_训练课程

keep://discover_course/

activity_热门活动

keep://hot_activities

hashtag_话题讨论

keep://hashtags_index

group_小组推荐

keep://groups_index/

kol_达人推荐

keep://recommend_keepers/

article_精选文章

keep://selections

热门

keep://timeline/hot

关注

keep://timeline/follow

逛逛

keep://timeline/wander

训练

keep://homepage/content?tabId=ZnVsbENvbnRlbnQ=

跑步

keep://homepage/running?tabId=cnVubmluZw==

瑜伽

keep://homepage/yoga?tabId=eW9nYQ==

行走

keep://homepage/hiking?tabId=aGlraW5n

骑行

keep://homepage/cycling?tabId=Y3ljbGluZw==

Kit

keep://homepage/keloton?tabId=a2Vsb3Rvbg==

数据中心

keep://datacenter?type=all&period=day

跑步历史记录

keep://datacenter?type=running&period=day

每周目标

keep://weeklypurpose

身体档案

keep://bodydata

运动能力

keep://physical_test_list

运动概况

keep://physical_summary

运动日记

https://show.gotokeep.com/usersfulldiary

步数记录

keep://steps_dashboard

连接应用和设备

keep://oauth/list

我的收藏

keep://my_favorites

我的活动

keep://activities

训练营历史

keep://bootcamp/history

我的路线

keep://my_running_routes

我的运动小队

https://show.gotokeep.com/outdoor/groups/list

我的最佳成绩

keep://running/best_records

我的 Class

keep://classes/mine

购物车

keep://shopping_cart

我的钱包

https://show.gotokeep.com/wallet

优惠券

keep://store_coupons

购买记录

keep://purchase_history

Keepland 课程

https://keepland.gotokeep.com/my_course?pulldownrefresh=true

从上面可以看到很多https://show.gotokeep.com开头的地址用Keep扫码也是可以直接进入的,但是我们自己的https网站却不能,可见Keep对自己的白名单内的域名做了特殊处理(相当于其他页面使用了keep://webview/事件)。

小应用

Keep并没有提供一种查看自己跑了多少个全马,或者跑了多少个半马这样的功能。现在我们写个脚本把自己的跑步数据存入我们自己的数据库中,并通过SQL查询出我们跑了多少个半马。

这里假设你已经安装了MySQL,并且已经建立了一个名叫keep_running的数据库。

建表脚本如下:

代码语言:javascript复制
DROP TABLE IF EXISTS running_data;

CREATE TABLE IF NOT EXISTS running_data (
  id VARCHAR(100) NULL,
  statsType VARCHAR(100) NULL,
  trainingCourseType VARCHAR(100) NULL,
  subtype VARCHAR(100) NULL,
  statsName VARCHAR(100) NULL,
  doneDate VARCHAR(100) NULL,
  icon VARCHAR(100) NULL,
  statsSchema VARCHAR(100) NULL,
  workoutFinishTimes INT NULL,
  duration INT NULL,
  distance INT NULL,
  steps INT NULL,
  kmDistance DOUBLE NULL,
  calorie INT NULL,
  averagePace DOUBLE NULL,
  averageSpeed DOUBLE NULL,
  exerciseInfo VARCHAR(500) NULL,
  statsStatus INT NULL,
  trackWaterMark VARCHAR(100) NULL,
  workoutId VARCHAR(100) NULL,
  vendorSource VARCHAR(100) NULL,
  vendorManufacturer VARCHAR(100) NULL,
  vendorGenre VARCHAR(100) NULL,
  vendorDeviceModel VARCHAR(100) NULL,
  vendorRecordId VARCHAR(100) NULL,
  heartRates TEXT NULL,
  averageHeartRate DOUBLE NULL,
  maxHeartRate VARCHAR(100) NULL,
  isDoubtful VARCHAR(100) NULL,
  logsType VARCHAR(100) NULL,
  statsDate VARCHAR(100) NULL,
  calorieSum INT NULL,
  durationSum INT NULL
);

要得到自己的跑步数据,可以调用https://api.gotokeep.com/pd/v3/stats/detail接口来获取,由于浏览器端会有跨域的问题,所以我们就直接跑一个node脚本就行了。

这里需要用到额外的两个库,一个是axios,用来发送http请求的;另一个库就是mysql,用来把数据存到数据库的。大家自己运行npm install一下。

脚本大致如下:

代码语言:javascript复制
const axios = require('axios').default
const mysql = require('mysql');
const connection = mysql.createConnection({
  host     : "localhost",
  user     : "root",
  password : "自己的数据库密码",
  database : "keep_running"
});

connection.connect();

queryAndInsert(connection);


let globalIndex = 1;

function queryAndInsert(connection,lastDate){
  axios.get('https://api.gotokeep.com/pd/v3/stats/detail', {
    params: { dateUnit: 'all',type:'running',lastDate },
    withCredentials: true,
    headers:{
      // TODO:这里是headers的数据,需要使用你自己的 不然查询会失败的
    }
  }).then(res=>{
    let sql =
    `INSERT INTO running_data (
      id, statsType, trainingCourseType, subtype, statsName,
      doneDate, icon, statsSchema, workoutFinishTimes, duration,
      distance, steps, kmDistance, calorie, averagePace,
      averageSpeed, exerciseInfo, statsStatus, trackWaterMark, workoutId,
      vendorSource, vendorManufacturer, vendorGenre, vendorDeviceModel, vendorRecordId,
      heartRates, averageHeartRate, maxHeartRate, isDoubtful, logsType,
      statsDate, calorieSum, durationSum)
      VALUES(?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?,?,?,?, ?,?,?);`
    let data = res.data.data
    let lastTimestamp = data.lastTimestamp
    if (lastTimestamp === 0 || !data.records || data.records.length === 0) {
      connection.end();
    } else {
      let posts = []
      data.records.forEach(record=>{
        (record.logs||[]).forEach(log=>{
          let stats = log.stats
          if (stats) {
            stats.vendor = stats.vendor || {}
            stats.heartRate = stats.heartRate || {}
            posts.push([
              stats.id, stats.type, stats.trainingCourseType, stats.subtype, stats.name,
              stats.doneDate, stats.icon, stats.schema, stats.workoutFinishTimes, stats.duration,
              stats.distance, stats.steps, stats.kmDistance, stats.calorie, stats.averagePace,
              stats.averageSpeed, stats.exerciseInfo, stats.status, stats.trackWaterMark, stats.workoutId,
              stats.vendor.source, stats.vendor.manufacturer, stats.vendor.genre, stats.vendor.deviceModel, stats.vendor.vendorRecordId,
              stats.heartRate.heartRates, stats.heartRate.averageHeartRate, stats.heartRate.maxHeartRate, stats.isDoubtful, log.type,
              record.date,  record.calorieSum,  record.durationSum
            ])
          }
        })
      })

      posts.forEach(post=>{
        connection.query(sql, post, function (error, results, fields) {
          if (error) throw error;
          console.log('成功插入了一条数据:'   globalIndex);
          globalIndex  ;
        });
      })

      queryAndInsert(connection,lastTimestamp)
    }
  })
}

注意上面headers处,需要换成自己数据的键值对形式,在Charles中获取方式如下:

最后你就可以通过SQL语句查询数据了:

代码语言:javascript复制
# 全马
SELECT * FROM running_data WHERE distance > 42195 ORDER BY distance DESC;

# 半马
SELECT * FROM running_data WHERE distance >= 21097.5 and  distance < 42195 ORDER BY distance DESC;

# 10公里
SELECT * FROM running_data WHERE distance >= 10000 and  distance < 21097.5 ORDER BY distance DESC;

# 5公里
SELECT * FROM running_data WHERE distance >= 5000 and  distance < 10000 ORDER BY distance DESC;

通过数据查询可以看出我自己跑了10个半马了?。

0 人点赞