Skip to content

Commit 3ac6f8c

Browse files
committed
Extract APK fetching & caching to a separate file
1 parent 50d14d8 commit 3ac6f8c

File tree

2 files changed

+149
-144
lines changed

2 files changed

+149
-144
lines changed

src/interceptors/android/android-adb-interceptor.ts

Lines changed: 3 additions & 144 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
import * as _ from 'lodash';
2-
import * as fs from 'fs';
32
import * as os from 'os';
4-
import * as path from 'path';
53
import * as crypto from 'crypto';
6-
import * as stream from 'stream';
74
import * as forge from 'node-forge';
8-
import * as semver from 'semver';
9-
import fetch from 'node-fetch';
105

116
import { Interceptor } from '..';
127
import { HtkConfig } from '../../config';
138
import { generateSPKIFingerprint } from 'mockttp';
9+
10+
import { reportError } from '../../error-tracking';
1411
import {
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

2622
function 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(/^httptoolkit-(.*).apk$/))
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(/^httptoolkit-(.*).apk$/))
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-
18544
export class AndroidAdbInterceptor implements Interceptor {
18645
readonly id = 'android-adb';
18746
readonly version = '1.0.0';
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
import * as fs from 'fs';
2+
import * as path from 'path';
3+
import * as stream from 'stream';
4+
import * as semver from 'semver';
5+
import fetch from 'node-fetch';
6+
7+
import { readDir, createTmp, renameFile, deleteFile } from '../../util';
8+
import { HtkConfig } from '../../config';
9+
import { reportError } from '../../error-tracking';
10+
11+
async function getLatestRelease(): Promise<{ version: string, url: string } | undefined> {
12+
try {
13+
const response = await fetch(
14+
"https://api.github.com/repos/httptoolkit/httptoolkit-android/releases/latest"
15+
);
16+
const release = await response.json();
17+
const apkAsset = release.assets.filter((a: any) => a.name === "httptoolkit.apk")[0];
18+
19+
// Ignore non-semver releases
20+
if (!semver.valid(release.name)) return;
21+
22+
return {
23+
version: release.name,
24+
url: apkAsset.browser_download_url
25+
};
26+
} catch (e) {
27+
console.log("Could not check latest Android app release", e);
28+
}
29+
}
30+
31+
async function getLocalApk(config: HtkConfig) {
32+
try {
33+
const apks = (await readDir(config.configPath))
34+
.map(filename => filename.match(/^httptoolkit-(.*).apk$/))
35+
.filter((match): match is RegExpMatchArray => !!match)
36+
.map((match) => ({
37+
path: path.join(config.configPath, match[0]),
38+
version: match[1]
39+
}));
40+
41+
apks.sort((apk1, apk2) => {
42+
return -1 * semver.compare(apk1.version, apk2.version);
43+
});
44+
45+
const latestLocalApk = apks[0];
46+
if (!latestLocalApk) return;
47+
else return latestLocalApk;
48+
} catch (e) {
49+
console.log("Could not check for local Android app APK", e);
50+
reportError(e);
51+
}
52+
}
53+
54+
async function updateLocalApk(
55+
version: string,
56+
apkStream: stream.Readable | NodeJS.ReadableStream,
57+
config: HtkConfig
58+
) {
59+
console.log(`Updating local APK to version ${version}`);
60+
61+
const {
62+
path: tmpApk,
63+
fd: tmpApkFd,
64+
cleanupCallback
65+
} = await createTmp({ keep: true });
66+
67+
const tmpApkStream = fs.createWriteStream(tmpApk, {
68+
fd: tmpApkFd,
69+
encoding: 'binary'
70+
});
71+
apkStream.pipe(tmpApkStream);
72+
73+
await new Promise((resolve, reject) => {
74+
apkStream.on('error', (e) => {
75+
reject(e);
76+
tmpApkStream.close();
77+
cleanupCallback();
78+
});
79+
tmpApkStream.on('error', (e) => {
80+
reject(e);
81+
cleanupCallback();
82+
});
83+
tmpApkStream.on('finish', () => resolve());
84+
});
85+
86+
console.log(`Local APK written to ${tmpApk}`);
87+
88+
await renameFile(tmpApk, path.join(config.configPath, `httptoolkit-${version}.apk`));
89+
console.log(`Local APK moved to ${path.join(config.configPath, `httptoolkit-${version}.apk`)}`);
90+
await cleanupOldApks(config);
91+
}
92+
93+
// Delete all but the most recent APK version in the config directory.
94+
async function cleanupOldApks(config: HtkConfig) {
95+
const apks = (await readDir(config.configPath))
96+
.map(filename => filename.match(/^httptoolkit-(.*).apk$/))
97+
.filter((match): match is RegExpMatchArray => !!match)
98+
.map((match) => ({
99+
path: path.join(config.configPath, match[0]),
100+
version: match[1]
101+
}));
102+
103+
apks.sort((apk1, apk2) => {
104+
return -1 * semver.compare(apk1.version, apk2.version);
105+
});
106+
107+
console.log(`Deleting old APKs: ${apks.slice(1).map(apk => apk.path).join(', ')}`);
108+
109+
return Promise.all(
110+
apks.slice(1).map(apk => deleteFile(apk.path))
111+
);
112+
}
113+
114+
export async function streamLatestApk(config: HtkConfig): Promise<stream.Readable> {
115+
const [latestApkRelease, localApk] = await Promise.all([
116+
await getLatestRelease(),
117+
await getLocalApk(config)
118+
]);
119+
120+
if (!localApk) {
121+
if (!latestApkRelease) {
122+
throw new Error("Couldn't find an Android APK locally or remotely");
123+
} else {
124+
console.log('Streaming remote APK directly');
125+
const apkStream = (await fetch(latestApkRelease.url)).body;
126+
updateLocalApk(latestApkRelease.version, apkStream, config);
127+
return apkStream as stream.Readable;
128+
}
129+
}
130+
131+
if (!latestApkRelease || semver.gte(localApk.version, latestApkRelease.version, true)) {
132+
console.log('Streaming local APK');
133+
// If we have an APK locally and it's up to date, or we can't tell, just use it
134+
return fs.createReadStream(localApk.path, { encoding: 'binary' });
135+
}
136+
137+
// We have a local APK & a remote APK, and the remote is newer.
138+
// Try to update it async, and use the local APK in the meantime.
139+
fetch(latestApkRelease.url).then((apkResponse) => {
140+
const apkStream = apkResponse.body;
141+
updateLocalApk(latestApkRelease.version, apkStream, config);
142+
}).catch(reportError);
143+
144+
console.log('Streaming local APK, and updating it async');
145+
return fs.createReadStream(localApk.path, { encoding: 'binary' });
146+
}

0 commit comments

Comments
 (0)