11import * as _ from 'lodash' ;
2- import * as fs from 'fs' ;
32import * as os from 'os' ;
4- import * as path from 'path' ;
53import * as crypto from 'crypto' ;
6- import * as stream from 'stream' ;
74import * as forge from 'node-forge' ;
8- import * as semver from 'semver' ;
9- import fetch from 'node-fetch' ;
105
116import { Interceptor } from '..' ;
127import { HtkConfig } from '../../config' ;
138import { generateSPKIFingerprint } from 'mockttp' ;
9+
10+ import { reportError } from '../../error-tracking' ;
1411import {
1512 ANDROID_TEMP ,
1613 createAdbClient ,
@@ -20,8 +17,7 @@ import {
2017 injectSystemCertificate ,
2118 stringAsStream
2219} from './adb-commands' ;
23- import { reportError } from '../../error-tracking' ;
24- import { readDir , createTmp , renameFile , deleteFile } from '../../util' ;
20+ import { streamLatestApk } from './fetch-apk' ;
2521
2622function urlSafeBase64 ( content : string ) {
2723 return Buffer . from ( content , 'utf8' ) . toString ( 'base64' )
@@ -45,143 +41,6 @@ function getCertificateHash(cert: forge.pki.Certificate) {
4541 . toString ( 16 ) ;
4642}
4743
48- async function getLatestRelease ( ) : Promise < { version : string , url : string } | undefined > {
49- try {
50- const response = await fetch (
51- "https://api.github.com/repos/httptoolkit/httptoolkit-android/releases/latest"
52- ) ;
53- const release = await response . json ( ) ;
54- const apkAsset = release . assets . filter ( ( a : any ) => a . name === "httptoolkit.apk" ) [ 0 ] ;
55-
56- // Ignore non-semver releases
57- if ( ! semver . valid ( release . name ) ) return ;
58-
59- return {
60- version : release . name ,
61- url : apkAsset . browser_download_url
62- } ;
63- } catch ( e ) {
64- console . log ( "Could not check latest Android app release" , e ) ;
65- }
66- }
67-
68- async function getLocalApk ( config : HtkConfig ) {
69- try {
70- const apks = ( await readDir ( config . configPath ) )
71- . map ( filename => filename . match ( / ^ h t t p t o o l k i t - ( .* ) .a p k $ / ) )
72- . filter ( ( match ) : match is RegExpMatchArray => ! ! match )
73- . map ( ( match ) => ( {
74- path : path . join ( config . configPath , match [ 0 ] ) ,
75- version : match [ 1 ]
76- } ) ) ;
77-
78- apks . sort ( ( apk1 , apk2 ) => {
79- return - 1 * semver . compare ( apk1 . version , apk2 . version ) ;
80- } ) ;
81-
82- const latestLocalApk = apks [ 0 ] ;
83- if ( ! latestLocalApk ) return ;
84- else return latestLocalApk ;
85- } catch ( e ) {
86- console . log ( "Could not check for local Android app APK" , e ) ;
87- reportError ( e ) ;
88- }
89- }
90-
91- async function updateLocalApk (
92- version : string ,
93- apkStream : stream . Readable | NodeJS . ReadableStream ,
94- config : HtkConfig
95- ) {
96- console . log ( `Updating local APK to version ${ version } ` ) ;
97-
98- const {
99- path : tmpApk ,
100- fd : tmpApkFd ,
101- cleanupCallback
102- } = await createTmp ( { keep : true } ) ;
103-
104- const tmpApkStream = fs . createWriteStream ( tmpApk , {
105- fd : tmpApkFd ,
106- encoding : 'binary'
107- } ) ;
108- apkStream . pipe ( tmpApkStream ) ;
109-
110- await new Promise ( ( resolve , reject ) => {
111- apkStream . on ( 'error' , ( e ) => {
112- reject ( e ) ;
113- tmpApkStream . close ( ) ;
114- cleanupCallback ( ) ;
115- } ) ;
116- tmpApkStream . on ( 'error' , ( e ) => {
117- reject ( e ) ;
118- cleanupCallback ( ) ;
119- } ) ;
120- tmpApkStream . on ( 'finish' , ( ) => resolve ( ) ) ;
121- } ) ;
122-
123- console . log ( `Local APK written to ${ tmpApk } ` ) ;
124-
125- await renameFile ( tmpApk , path . join ( config . configPath , `httptoolkit-${ version } .apk` ) ) ;
126- console . log ( `Local APK moved to ${ path . join ( config . configPath , `httptoolkit-${ version } .apk` ) } ` ) ;
127- await cleanupOldApks ( config ) ;
128- }
129-
130- // Delete all but the most recent APK version in the config directory.
131- async function cleanupOldApks ( config : HtkConfig ) {
132- const apks = ( await readDir ( config . configPath ) )
133- . map ( filename => filename . match ( / ^ h t t p t o o l k i t - ( .* ) .a p k $ / ) )
134- . filter ( ( match ) : match is RegExpMatchArray => ! ! match )
135- . map ( ( match ) => ( {
136- path : path . join ( config . configPath , match [ 0 ] ) ,
137- version : match [ 1 ]
138- } ) ) ;
139-
140- apks . sort ( ( apk1 , apk2 ) => {
141- return - 1 * semver . compare ( apk1 . version , apk2 . version ) ;
142- } ) ;
143-
144- console . log ( `Deleting old APKs: ${ apks . slice ( 1 ) . map ( apk => apk . path ) . join ( ', ' ) } ` ) ;
145-
146- return Promise . all (
147- apks . slice ( 1 ) . map ( apk => deleteFile ( apk . path ) )
148- ) ;
149- }
150-
151- async function streamLatestApk ( config : HtkConfig ) : Promise < stream . Readable > {
152- const [ latestApkRelease , localApk ] = await Promise . all ( [
153- await getLatestRelease ( ) ,
154- await getLocalApk ( config )
155- ] ) ;
156-
157- if ( ! localApk ) {
158- if ( ! latestApkRelease ) {
159- throw new Error ( "Couldn't find an Android APK locally or remotely" ) ;
160- } else {
161- console . log ( 'Streaming remote APK directly' ) ;
162- const apkStream = ( await fetch ( latestApkRelease . url ) ) . body ;
163- updateLocalApk ( latestApkRelease . version , apkStream , config ) ;
164- return apkStream as stream . Readable ;
165- }
166- }
167-
168- if ( ! latestApkRelease || semver . gte ( localApk . version , latestApkRelease . version , true ) ) {
169- console . log ( 'Streaming local APK' ) ;
170- // If we have an APK locally and it's up to date, or we can't tell, just use it
171- return fs . createReadStream ( localApk . path , { encoding : 'binary' } ) ;
172- }
173-
174- // We have a local APK & a remote APK, and the remote is newer.
175- // Try to update it async, and use the local APK in the meantime.
176- fetch ( latestApkRelease . url ) . then ( ( apkResponse ) => {
177- const apkStream = apkResponse . body ;
178- updateLocalApk ( latestApkRelease . version , apkStream , config ) ;
179- } ) . catch ( reportError ) ;
180-
181- console . log ( 'Streaming local APK, and updating it async' ) ;
182- return fs . createReadStream ( localApk . path , { encoding : 'binary' } ) ;
183- }
184-
18544export class AndroidAdbInterceptor implements Interceptor {
18645 readonly id = 'android-adb' ;
18746 readonly version = '1.0.0' ;
0 commit comments