7

Landon's Blog

 2 years ago
source link: https://blog.landon.li/2020/06/21/%E5%8C%97%E4%BA%AC%E5%AE%9E%E6%97%B6%E5%85%AC%E4%BA%A4/?amp%3Butm_medium=rss&%3Butm_campaign=%25e5%258c%2597%25e4%25ba%25ac%25e5%25ae%259e%25e6%2597%25b6%25e5%2585%25ac%25e4%25ba%25a4
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.
neoserver,ios ssh client

Landon's Blog

北京实时公交

2020-6-21 19:29
1120 字
本文最后更新于 618 天前,其中的信息可能已经有所发展或是发生改变。

公司离地铁站有一站路的距离,每天下班的时候等一班公交的时间有点长。如果能掌握下一班车的准确到达时间,就能合理地安排出发时间,减少在车站里等候的时间。

有个叫北京实时公交的 app 提供北京市内许多公交的实时车况信息。

安装 app 后,通过抓包工具,可以获取 app 从接口请求的数据

全部线路信息

 Request Head:
 ​
 GET /ssgj/v1.0.0/checkupdate?version=0 HTTP/1.1
 ABTOKEN: 5e428c5daf4c297d1cec300af7684361
 NETWORK: gprs
 PKG_SOURCE: 1
 PID: 5
 IMEI: ***已移除***
 CTYPE: json
 TIME: 1592734115
 HEADER_KEY_SECRET: bjjw_jtcx
 UA: ***已移除***
 SID:
 VID: 6
 PLATFORM: android
 UID:
 CUSTOM: aibang
 SOURCE: 1
 IMSI: ***已移除***
 CID: 67a88ec31de7a589a2344cc5d0469074
 Host: transapp.btic.org.cn:8512
 Connection: Keep-Alive
 Accept-Encoding: gzip
 User-Agent: okhttp/3.3.1
 ​
 ​
 Body:
 ​
 ​
 ​
 Response Head:
 ​
 HTTP/1.1 200 OK
 Server: nginx/1.14.2
 Date: Sun, 21 Jun 2020 10:05:09 GMT
 Content-Type: application/json;charset=utf-8
 Transfer-Encoding: chunked
 Connection: keep-alive
 X-Powered-By: PHP/5.4.0
 Content-Encoding: gzip
 ​
 ​
 Body:
 ​
 {"errcode":"200","errmsg":"success","updateNum":"2941","lineNum":"1192","dataversion":"v1.1.0","lines":{"line":[{"id":"1","linename":"1(四惠枢纽站-老山公交场站)","classify":"1-999路","status":"0","version":"163"},{"id":"2","linename":"1(老山公交场站-四惠枢纽站)","classify":"1-999路","status":"0","version":"163"},{"id":"1195","linename":"机场巴士1(T3-方庄)","classify":"1-999路","status":"1","version":"5"},{"id":"1196","linename":"机场巴士1(方庄-T3)","classify":"1-999路","status":"1","version":"5"},{"id":"15","linename":"10(南菜园-航天桥东)","classify":"1-999路","status":"0","version":"114"},{"id":"16","linename":"10(航天桥东-南菜园)","classify":"1-999路","status":"0","version":"114"},{"id":"1201","linename":"机场巴士10(T3-北京南站)","classify":"1-999路","status":"1","version":"5"},{"id":"1202","linename":"机场巴士10(北京南站-T3)","classify":"1-999路","status":"1","version":"5"},

线路详细信息

 ​
 Request Head:
 ​
 GET /ssgj/v1.0.0/update?id=2361 HTTP/1.1
 ABTOKEN: 9aa487c715154be434557d7865cd6ee4
 NETWORK: gprs
 PKG_SOURCE: 1
 PID: 5
 IMEI: ***已移除***
 CTYPE: json
 TIME: 1592734149
 HEADER_KEY_SECRET: bjjw_jtcx
 UA: ***已移除***
 SID:
 VID: 6
 PLATFORM: android
 UID:
 CUSTOM: aibang
 SOURCE: 1
 IMSI: ***已移除***
 CID: 67a88ec31de7a589a2344cc5d0469074
 Host: transapp.btic.org.cn:8512
 Connection: Keep-Alive
 Accept-Encoding: gzip
 User-Agent: okhttp/3.3.1
 ​
 ​
 Body:
 ​
 ​
 ​
 Response Head:
 ​
 HTTP/1.1 200 OK
 Server: nginx/1.14.2
 Date: Sun, 21 Jun 2020 10:05:43 GMT
 Content-Type: application/json;charset=utf-8
 Transfer-Encoding: chunked
 Connection: keep-alive
 X-Powered-By: PHP/5.4.0
 Content-Encoding: gzip
 ​
 ​
 Body:
 ​
 {
  "errcode":"200",
  "errmsg":"success",
  "busline":[
  {
  "lineid":"2361",
  "shotname":"ekSl",
  "linename":"ekSl2mI79LuvNBh06CJ77O4BqzyJgjjCKkJnWeEw",
  "distince":"0.00",
  "ticket":"分段计价",
  "totalPrice":"0.00",
  "time":"5:30-23:00",
  "type":"0",
  "coord":"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",
  "status":"0",
  "version":"211",
  "stations":{
  "station":[
  {
  "name":"rP83Fj0LsMS6ZVhQ",
  "no":"eg==",
  "lon":"ekeg3LaEYmsu",
  "lat":"eE+4y7KEYGwh"
  },
  {
  "name":"rP83Fj0LsMS6",
  "no":"eQ==",
  "lon":"ekeg3LaEbGci",
  "lat":"eE+4y7KHYGg="
  },
  {
  "name":"rvoBFyEYsOaka1xKoXJC",
  "no":"eA==",
  "lon":"ekeg3LaFZ2gv",
  "lat":"eE+4y7OFYm4m"
  },
  {
  "name":"ot86FREKs/6yaHB4",
  "no":"fw==",
  "lon":"ekeg3LaFbGYm",
  "lat":"eE+4y7OEZW0="
  },
  {
  "name":"rsghFyEksNqkaHNN",
  "no":"fg==",
  "lon":"ekeg3LaFbG4vuA==",
  "lat":"eE+4y7CLYGol"
  },
  {
  "name":"os0SFyoIsPuwZVx4rW9htkxL",
  "no":"fQ==",
  "lon":"ekeg3LaKZWov",
  "lat":"eE+4y7OAbW0="
  },
  {
  "name":"ou4lFwA7seeqa2RAoHpRtk9S",
  "no":"fA==",
  "lon":"ekeg3LaKYGch",
  "lat":"eE+4y7OAbGs="
  },
  {
  "name":"os0SFyoI",
  "no":"cw==",
  "lon":"ekeg3LaKbG0h",
  "lat":"eE+4y7OBZW0v"
  },
  {
  "name":"os0SFyoIsPuwZVx4oXJCtkxL",
  "no":"cg==",
  "lon":"ekeg3LaLZm4i",
  "lat":"eE+4y7OBZGs="
  },
  {
  "name":"rtgfFwg8vfqoZHpj",
  "no":"ekY=",
  "lon":"ekeg3LaLYm0g",
  "lat":"eE+4y7OBZ2o="
  },
  {
  "name":"rtIAGyM0s8mLZVx4",
  "no":"ekc=",
  "lon":"ekeg3LGCZ24l",
  "lat":"eE+4y7OBZmk="
  },
  {
  "name":"rsYZGz42sOWT",
  "no":"ekQ=",
  "lon":"ekeg3LGCbG4=",
  "lat":"eE+4y7OAZW4="
  },
  {
  "name":"ousEFzwGs+2IZVhQoEV9",
  "no":"ekU=",
  "lon":"ekeg3LGDZ2gv",
  "lat":"eE+4y7ODbGgh"
  },
  {
  "name":"ruQaFzwBvNibZVhQrWtJ",
  "no":"ekI=",
  "lon":"ekeg3LGDY2Yj",
  "lat":"eE+4y7OAZWs="
  },
  {
  "name":"ousEFzwGs+2IaUVzoEV9",
  "no":"ekM=",
  "lon":"ekeg3LGAYW0k",
  "lat":"eE+4y7OAZGgi"
  },
  {
  "name":"ruQaFzwBvNibZUpAoEV9t3t0",
  "no":"ekA=",
  "lon":"ekeg3LGAY28g",
  "lat":"eE+4y7CLZmo="
  },
  {
  "name":"ruomGxYzs8CkZXdcomFH",
  "no":"ekE=",
  "lon":"ekeg3LGBZ28jvg==",
  "lat":"eE+4y7CKY2sv"
  },
  {
  "name":"r84KFxktsMCZZUpA",
  "no":"ek4=",
  "lon":"ekeg3LGBZ2wl",
  "lat":"eE+4y7CGbWY="
  },
  {
  "name":"r84KFxktsMCZZUpAoEdJtkxL",
  "no":"ek8=",
  "lon":"ekeg3LGBZ2sm",
  "lat":"eE+4y7CAbGs="
  },
  {
  "name":"rvoBFysqsNGS",
  "no":"eUY=",
  "lon":"ekeg3LGBZWsl",
  "lat":"eE+4y7GLbGo="
  },
  {
  "name":"r84KFR4GvMi/aHF4",
  "no":"eUc=",
  "lon":"ekeg3LGBZmo=",
  "lat":"eE+4y7GHZm4="
  },
  {
  "name":"r84KFR4GvMi/",
  "no":"eUQ=",
  "lon":"ekeg3LGBY2wu",
  "lat":"eE+4y7GDZWg="
  },
  {
  "name":"re4zFSIyvei4aHJMoEZJ",
  "no":"eUU=",
  "lon":"ekeg3LGGYWg=",
  "lat":"eE+4y7GAYA=="
  },
  {
  "name":"re4zFSIyvei4aHF4oEV9",
  "no":"eUI=",
  "lon":"ekeg3LGGYWsi",
  "lat":"eE+4y7GEYmk="
  },
  {
  "name":"otAPFDcBsMS6a1xK",
  "no":"eUM=",
  "lon":"ekeg3LGGZGgv",
  "lat":"eE+4y7GLZg=="
  }
  ]
  }
  }
  ]
 }

实时公交信息

 ​
 Request Head:
 ​
 GET /ssgj/bus.php?no=22&versionid=6&city=%E5%8C%97%E4%BA%AC&datatype=json&encrypt=1&id=2361&type=0 HTTP/1.1
 ABTOKEN: 2c0f7826f353da66b014fc570933721a
 NETWORK: gprs
 PKG_SOURCE: 1
 PID: 5
 IMEI: ***已移除***
 CTYPE: json
 TIME: 1592734157
 HEADER_KEY_SECRET: bjjw_jtcx
 UA: ***已移除***
 SID:
 VID: 6
 PLATFORM: android
 UID:
 CUSTOM: aibang
 SOURCE: 1
 IMSI: ***已移除***
 CID: 67a88ec31de7a589a2344cc5d0469074
 Host: transapp.btic.org.cn:8512
 Connection: Keep-Alive
 Accept-Encoding: gzip
 User-Agent: okhttp/3.3.1
 ​
 ​
 Body:
 ​
 ​
 ​
 Response Head:
 ​
 HTTP/1.1 200 OK
 Server: nginx/1.14.2
 Date: Sun, 21 Jun 2020 10:05:51 GMT
 Content-Type: application/json;charset=utf-8
 Transfer-Encoding: chunked
 Connection: keep-alive
 X-Powered-By: PHP/5.4.0
 Content-Encoding: gzip
 ​
 ​
 Body:
 ​
 {
  "root":{
  "status":"200",
  "message":"success",
  "encrypt":"1",
  "num":"4",
  "lid":"2361",
  "data":{
  "bus":[
  {
  "gt":"1592734135",
  "id":"19934",
  "t":"0",
  "ns":"3Nhrf0eYcf0vlVos",
  "nsn":"CE0=",
  "nsd":"250",
  "nsrt":"63",
  "nst":"1592734198",
  "sd":"D0rNpA==",
  "srt":"CE3Org==",
  "st":"CEnEpNYtolmETg==",
  "crowding":"0",
  "x":"CE3LuNInrlqL",
  "y":"CkXTr9ctpFmL",
  "lt":"0",
  "ut":"1592734106"
  },
  {
  "gt":"1592734112",
  "id":"19990",
  "t":"0",
  "ns":"rUSGzlzCPnp1",
  "nsn":"e84=",
  "nsd":"-1",
  "nsrt":"-1",
  "nst":"-1",
  "sd":"ZM0=",
  "srt":"ZM0=",
  "st":"ZM0=",
  "crowding":"0",
  "x":"eM0sB/NF4d/v",
  "y":"esU0EPNH59vk",
  "lt":"0",
  "ut":"1592734086"
  },
  {
  "gt":"1592734094",
  "id":"20404",
  "t":"0",
  "ns":"1W3aRWz7ReWblKCC",
  "nsn":"CA==",
  "nsd":"50",
  "nsrt":"3",
  "nst":"1592734097",
  "sd":"BfJElQ==",
  "srt":"DfJAkQ==",
  "st":"DfFPkM9wlnMLRg==",
  "crowding":"0",
  "x":"DfVAjMt0mn0LRg==",
  "y":"D/1Ym851l3MJ",
  "lt":"0",
  "ut":"1592734061"
  },
  {
  "gt":"1592733994",
  "id":"19933",
  "t":"0",
  "ns":"P5ohr7huq7FuvGlhKF8p",
  "nsn":"6zE=",
  "nsd":"414",
  "nsrt":"73",
  "nst":"1592734067",
  "sd":"9DM=",
  "srt":"9DM=",
  "st":"9DM=",
  "crowding":"0",
  "x":"6DOyZivacjP3bQ==",
  "y":"6juqcSvfcjf3",
  "lt":"0",
  "ut":"1592733991"
  }
  ]
  }
  }
 }

从上面三个接口的返回数据可以看到,除了全部线路信息,线路详细信息和实时公交信息返回的数据都是加密过的,仅从抓包结果无法得到解密后的信息。这个时候就需要反编译 apk,通过查看源码来分析解密算法了。

请出 dex2jarJD-GUI,通过 dex2jar 把 apk 导出为 jar 包,通过 JD-GUI 反编译出源码。

经过在源码中的一番探索,可以发现线路详细信息的类对应的是 com.aibang.nextbus.modle.DetailLine,实时公交信息对应的类是 com.aibang.nextbus.modle.Bus

线路详细信息

首先查看 DetailLine 类,发现里面有个 decode 方法在解密数据。此方法调用了 com.aibang.nextbus.security.NextBusSecurityUtils 类的 decry 方法,并传入了加密的数据和 getKey 方法的返回值。

查看 getKey 方法,返回的是拼接字符串 aibang 和线路 ID 的 MD5 值,即 md5("aibang"+lineid)

lineid 从全部线路信息返回数据中获取

查看 decry 方法,先将加密的数据进行 base64.decode,然后和 getKey 的返回值一起进行 RC4Base

Java 和 Python 的 byte 范围不一样。Java 中 byte 的范围是 - 127-128,Python 中 byte 的范围是 0-256。在进行 string 和 bytes 互转时要考虑到这个问题。

有了源码,很容易地能写出对应的 Python 版

 class RC4:
     def initKey(self, paramString: str):
         arrayOfByte2 = paramString.encode()
         arrayOfByte1 = []
         for i in range(256):
             if i <= 127:
                 arrayOfByte1.append(i)
             else:
                 arrayOfByte1.append(-128 + i - 128)
         j = k = 0
         if arrayOfByte2 is None or len(arrayOfByte2) == 0:
             return None
         i = 0
         while True:
             arrayOfByte = arrayOfByte1
             if (i < 256):
                 k = (arrayOfByte2[j] & 0xFF) + (arrayOfByte1[i] & 0xFF) + k & 0xFF
                 arrayOfByte1[i], arrayOfByte1[k] = arrayOfByte1[k], arrayOfByte1[i]
                 j = (j + 1) % len(arrayOfByte2)
                 i += 1
                 continue
 ​
             return arrayOfByte
 ​
     def RC4Base(self, paramArrayOfByte: List[int], paramString: str) -> List[int]:
         k = j = 0
         arrayOfByte1 = self.initKey(paramString)
         arrayOfByte2 = [0] * len(paramArrayOfByte)
         for i in range(len(paramArrayOfByte)):
             k = k + 1 & 0xFF
             j = (arrayOfByte1[k] & 0xFF) + j & 0xFF
             arrayOfByte1[j], arrayOfByte1[k] = arrayOfByte1[k], arrayOfByte1[j]
             b2 = arrayOfByte1[k]
             b3 = arrayOfByte1[j]
             arrayOfByte2[i] = (paramArrayOfByte[i] ^ arrayOfByte1[(b2 & 0xFF) + (b3 & 0xFF) & 0xFF])
             i += 1
         return arrayOfByte2

RC4 部分照抄 Java 版的代码,要注意的是 str 和 bytes 互转的时候需要额外地处理。

 def decode(ciphertext: str, param: str) -> str:
     key = get_md5(f'aibang{param}')
     step1 = [byte if byte <= 127 else byte - 256 for byte in base64.b64decode(ciphertext.encode())]
     step2 = [byte if byte > 0 else byte + 256 for byte in rc4.RC4Base(step1, key)]
     result = bytes(step2).decode('utf-8')
     return result

这样就能写出根据 lineid 从接口的返回值中解密数据的代码

 def get_line_detail(line_id):
     line_id = str(line_id)
     url = base_url + f'/ssgj/v1.0.0/update?id={line_id}'
     result = do_get(url)
     if result is None or result.get('errcode', '-1') != '200':
         raise RuntimeError('返回数据错误!')
     busline = result.get('busline')[0]
     line_name = decode(busline.get('linename'), line_id)
     runtime = busline.get('time')
 ​
     datas = []
     datas.append(f'{line_name} {runtime}\n')
     stations = busline.get('stations', {}).get('station')
     for station in stations:
         station_name = decode(station.get('name'), line_id)
         station_id = decode(station.get('no'), line_id)
         datas.append(f'{int(station_id):2d} {station_name}\n')
 ​
     with open(f'line_{line_id}.txt', 'w', encoding='utf-8') as f:
         f.writelines(datas)

比如 123 路公交牡丹园西 - 香河园桥方向的线路信息是

 123(牡丹园西-香河园桥) 5:30-23:00
  1 牡丹园西
  2 牡丹园
  3 北太平桥东
  4 马甸桥南
  5 德外关厢
  6 黄寺大街西口
  7 阳光丽景小区
  8 黄寺
  9 黄寺大街东口
 10 安华西里
 11 外馆斜街
 12 小黄庄
 13 青年沟西口
 14 和平里西街
 15 青年沟东口
 16 和平里路口东
 17 地铁柳芳站
 18 东土城路
 19 东土城路南口
 20 北官厅
 21 东直门北
 22 东直门
 23 春秀路口北
 24 春秀路北口
 25 香河园桥

实时公交信息

上一步最后获取了 lineid 对应线路的站点信息,通过 lineidstationid,可以获取线路上运行的公交与指定站点之间的实时信息。

com.aibang.nextbus.modle.Bus 中,也有一个 decode 方法。可以看到此方法的实现和之前 DetailLine 中的实现类似,不过 Bus 类中的 getKey 方法传入的是 aibanggpsupdateTime 拼接的字符串。

通过类成员的定义,可以看到 gpsupdateTime 对应的是返回数据中的 gt 字段。

至此,不难写出根据 lineidstationid 获取实时公交信息的代码

 def get_realtime_bus(line_id, station_id):
     url = base_url + f'/ssgj/bus.php?no={station_id}&versionid=6&city=%E5%8C%97%E4%BA%AC&datatype=json&encrypt=1&id={line_id}&type=1 '
     result = do_get(url)
     if result is None or result.get('root', {}).get('status', '-1') != '200':
         raise RuntimeError('获取数据错误')
 ​
     buses = result.get('root', {}).get('data', {}).get('bus')
 ​
     for bus in buses:
         gps_update_time = bus.get('gt')
         bus_id = bus.get('id')
         next_station = decode(bus.get('ns'), gps_update_time)
         next_station_no = decode(bus.get('nsn'), gps_update_time)
         distance = decode(bus.get('sd'), gps_update_time)
         remain_seconds = decode(bus.get('srt'), gps_update_time)
         arriving_time = decode(bus.get('st'), gps_update_time)
         if arriving_time == '-1':
             continue
         arriving_time = datetime.fromtimestamp(int(arriving_time))
 ​
         print(
             f'车辆No.{bus_id}下一站{next_station_no}-{next_station},距离目的站点还有{int(station_id) - int(next_station_no) + 1}站、{distance}米,预计{arriving_time}到达,还有{remain_seconds}s'
        )

例如 123 路公交牡丹园西 - 香河园桥方向东直门站在 2020-06-21 18:55 时的实时公交信息是

ABTOKEN?

然而问题还没有彻底解决,之前直接用的从抓包信息中获取的 Header。其中 TIME 字段是时间戳,ABTOKEN 字段每次都会改变,可以猜测是根据时间戳计算而来的。超过一定时间之后,原有的 TIMEABTOKEN 的组合会失效。所以还需要找到 ABTOKEN 的计算方法。

继续在源码中探索,发现 com.aibang.nextbus.okhttp.HeaderSetHelper 这个类的 setHeader 方法在操作 Header。

查看代码可以得知,ABTOKEN 这个参数是给 Utils.generateToken 方法传入 5 个参数:bjjw_jtcxandroid67a88ec31de7a589a2344cc5d0469074时间戳paramString 后的返回值。其中前三个参数是固定值,时间戳容易计算。

com.aibang.nextbus.okhttp.NextbusHttpRequest 中可以看到调用 setHeader 方法时传入的实参,第 5 个参数 paramStringgetPath 方法的返回值,即发送 GET 请求时的 URL 中的 Path 部分。

继续查看 com.aibang.common.util.Utils 类中的 generateToken 方法,可以看到返回值 是将传入的参数全部拼接后先 sha1 再 md5 的结果。

根据原理,可以写出对应的代码

def get_abtoken(timestamp, path):
text = f'bjjw_jtcxandroid67a88ec31de7a589a2344cc5d0469074{timestamp}{path}'
abtoken = get_md5(get_sha1(text))
return abtoken

至此,可以通过 Python 代码获取指定线路指定站点的实时公交信息了。

完整代码


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK