Skip to content

Commit b3e28aa

Browse files
committed
Minor improvements for base64 content + HAR _content is always base64-encoded
1 parent f5ec9ee commit b3e28aa

File tree

5 files changed

+48
-21
lines changed

5 files changed

+48
-21
lines changed

src/components/intercept/config/android-device-config.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,7 @@ const Spacer = styled.div`
6464
`;
6565

6666
function urlSafeBase64(content: string) {
67-
return stringToBuffer(content)
68-
.toString('base64')
69-
.replace(/\+/g, '-')
70-
.replace(/\//g, '_');
67+
return stringToBuffer(content).toString('base64url');
7168
}
7269

7370
function getConfigRequestIds(eventsStore: EventsStore) {

src/model/events/content-types.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -147,15 +147,25 @@ export function getDefaultMimeType(contentType: ViewableContentType): string {
147147
return _.findKey(mimeTypeToContentTypeMap, (c) => c === contentType)!;
148148
}
149149

150-
function isValidBase64Byte(byte: number) {
150+
function isValidAlphaNumOrSpace(byte: number) {
151151
return (byte >= 65 && byte <= 90) || // A-Z
152152
(byte >= 97 && byte <= 122) || // a-z
153153
(byte >= 48 && byte <= 57) || // 0-9
154-
byte === 43 || // +
155-
byte === 47 || // /
156154
byte === 61; // =
157155
}
158156

157+
function isValidStandardBase64Byte(byte: number) {
158+
// + / (standard)
159+
return byte === 43 || byte === 47
160+
|| isValidAlphaNumOrSpace(byte);
161+
}
162+
163+
function isValidURLSafeBase64Byte(byte: number) {
164+
// - _ (URL-safe version)
165+
return byte === 45 || byte === 95
166+
|| isValidAlphaNumOrSpace(byte);
167+
}
168+
159169
export function getCompatibleTypes(
160170
contentType: ViewableContentType,
161171
rawContentType: string | undefined,
@@ -203,10 +213,11 @@ export function getCompatibleTypes(
203213

204214
if (
205215
body &&
206-
body.length > 0 &&
207-
body.length % 4 === 0 && // Multiple of 4 bytes
208-
body.length < 1000 * 100 && // < 100 KB of content
209-
body.every(isValidBase64Byte)
216+
!types.has('base64') &&
217+
body.length >= 8 &&
218+
// body.length % 4 === 0 && // Multiple of 4 bytes (final padding may be omitted)
219+
body.length < 100_000 && // < 100 KB of content
220+
(body.every(isValidStandardBase64Byte) || body.every(isValidURLSafeBase64Byte))
210221
) {
211222
types.add('base64');
212223
}

src/model/http/har.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,15 @@ interface HarLog extends HarFormat.Log {
5151
export type RequestContentData = {
5252
text: string;
5353
size: number;
54-
encoding?: 'base64';
54+
encoding: 'base64';
5555
comment?: string;
5656
};
5757

5858
export interface ExtendedHarRequest extends HarFormat.Request {
5959
_requestBodyStatus?:
60-
| 'discarded:too-large'
61-
| 'discarded:not-representable'
62-
| 'discarded:not-decodable';
60+
| 'discarded:too-large'
61+
| 'discarded:not-representable' // to indicate that extended field `_content` is populated with base64 `postData`
62+
| 'discarded:not-decodable';
6363
_content?: RequestContentData;
6464
_trailers?: HarFormat.Header[];
6565
}
@@ -302,7 +302,7 @@ async function generateHarResponse(
302302

303303
const decoded = await response.body.decodedPromise;
304304

305-
let responseContent: { text: string, encoding?: string } | { comment: string};
305+
let responseContent: { text: string, encoding?: string } | { comment: string };
306306
try {
307307
if (!decoded || decoded.byteLength > options.bodySizeLimit) {
308308
// If no body or the body is too large, don't include it
@@ -435,10 +435,10 @@ function generateHarWebSocketMessage(
435435
return {
436436
// Note that msg.direction is from the perspective of Mockttp, not the client.
437437
type: message.direction === 'sent'
438-
? 'receive'
438+
? 'receive'
439439
: message.direction === 'received'
440440
? 'send'
441-
: unreachableCheck(message.direction),
441+
: unreachableCheck(message.direction),
442442

443443
opcode: message.isBinary ? 2 : 1,
444444
data: message.isBinary
@@ -751,7 +751,7 @@ function parseHttpVersion(
751751
}
752752

753753
function parseHarRequestContents(data: RequestContentData): Buffer {
754-
if (data.encoding && Buffer.isEncoding(data.encoding)) {
754+
if (Buffer.isEncoding(data.encoding)) {
755755
return Buffer.from(data.text, data.encoding);
756756
}
757757

src/services/ui-worker-formatters.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ const WorkerFormatters = {
3939
}
4040
},
4141
base64: (content: Buffer) => {
42-
return Buffer.from(content.toString('utf8'), 'base64').toString('utf8');
42+
const b64 = content.toString('ascii');
43+
const encoding = b64.match(/[-_]/) ? 'base64url' : 'base64';
44+
return Buffer.from(b64, encoding).toString('utf8');
4345
},
4446
markdown: (content: Buffer) => {
4547
return content.toString('utf8');

test/unit/model/http/content-types.spec.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { expect } from '../../../test-setup';
22

3-
import { getContentType, getEditableContentType } from '../../../../src/model/events/content-types';
3+
import { getContentType, getEditableContentType, getCompatibleTypes } from '../../../../src/model/events/content-types';
44

55
describe('Content type parsing', () => {
66
describe('getContentType', () => {
@@ -81,4 +81,21 @@ describe('Content type parsing', () => {
8181
expect(ct).to.equal(undefined);
8282
});
8383
});
84+
85+
describe('getCompatibleTypes', () => {
86+
it('should detect standard base64 text', () => {
87+
const cts = getCompatibleTypes('text', 'text/plain', Buffer.from('FWTkm2+ZvMo=', 'ascii'));
88+
expect(cts).to.deep.equal(['text', 'base64', 'raw']);
89+
});
90+
91+
it('should detect URL-safe (without padding) base64 text', () => {
92+
const cts = getCompatibleTypes('text', 'text/plain', Buffer.from('FWTkm2-ZvMo', 'ascii'));
93+
expect(cts).to.deep.equal(['text', 'base64', 'raw']);
94+
});
95+
96+
it('should work even if first character is not ASCII', () => {
97+
const cts = getCompatibleTypes('raw', 'application/octet-stream', Buffer.from('1f8d08', 'hex')); // GZIP magic bytes
98+
expect(cts).to.deep.equal(['raw', 'text']);
99+
});
100+
});
84101
});

0 commit comments

Comments
 (0)