Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
ulixee
GitHub Repository: ulixee/secret-agent
Path: blob/main/mitm/handlers/CacheHandler.ts
1030 views
1
import HttpResponseCache from '../lib/HttpResponseCache';
2
import IMitmRequestContext from '../interfaces/IMitmRequestContext';
3
import ResourceState from '../interfaces/ResourceState';
4
import HeadersHandler from './HeadersHandler';
5
6
export default class CacheHandler {
7
public static isEnabled = Boolean(JSON.parse(process.env.SA_ENABLE_MITM_CACHE ?? 'false'));
8
public didProposeCachedResource = false;
9
public shouldServeCachedData = false;
10
private readonly data: Buffer[] = [];
11
12
public get buffer(): Buffer {
13
return Buffer.concat(this.data);
14
}
15
16
public get cacheData(): Buffer | null {
17
if (!this.shouldServeCachedData) return null;
18
return this.buffer;
19
}
20
21
constructor(readonly responseCache: HttpResponseCache, readonly ctx: IMitmRequestContext) {}
22
23
public onRequest(): void {
24
const ctx = this.ctx;
25
ctx.setState(ResourceState.CheckCacheOnRequest);
26
if (!CacheHandler.isEnabled) return;
27
28
// only cache get (don't do preflight, post, etc)
29
if (ctx.method === 'GET' && !HeadersHandler.getRequestHeader(ctx, 'if-none-match')) {
30
const cache = this.responseCache?.get(ctx.url.href);
31
32
if (cache?.etag) {
33
const key = this.ctx.isServerHttp2 ? 'if-none-match' : 'If-None-Match';
34
ctx.requestHeaders[key] = cache.etag;
35
this.didProposeCachedResource = true;
36
}
37
}
38
}
39
40
public onHttp2PushStream(): void {
41
this.ctx.setState(ResourceState.CheckCacheOnRequest);
42
if (!CacheHandler.isEnabled) return;
43
if (this.ctx.method === 'GET') {
44
const cached = this.responseCache?.get(this.ctx.url.href);
45
if (cached) {
46
this.didProposeCachedResource = true;
47
this.useCached();
48
}
49
}
50
}
51
52
public onResponseData(chunk: Buffer): Buffer {
53
let data = chunk;
54
if (this.shouldServeCachedData) {
55
data = null;
56
} else if (chunk) {
57
this.data.push(chunk);
58
}
59
return data;
60
}
61
62
public onResponseHeaders(): void {
63
if (!CacheHandler.isEnabled) return;
64
if (this.didProposeCachedResource && this.ctx.status === 304) {
65
this.useCached();
66
this.ctx.status = 200;
67
}
68
}
69
70
public onResponseEnd(): void {
71
const ctx = this.ctx;
72
ctx.setState(ResourceState.CheckCacheOnResponseEnd);
73
if (!CacheHandler.isEnabled) return;
74
if (
75
ctx.method === 'GET' &&
76
!this.didProposeCachedResource &&
77
!ctx.didBlockResource &&
78
this.data.length
79
) {
80
const resHeaders = ctx.responseHeaders;
81
this.responseCache?.add(ctx.url.href, Buffer.concat(this.data), resHeaders);
82
}
83
}
84
85
private useCached(): void {
86
const { responseHeaders, url } = this.ctx;
87
const cached = this.responseCache?.get(url.href);
88
let isLowerKeys = false;
89
for (const key of Object.keys(responseHeaders)) {
90
if (key.toLowerCase() === key) isLowerKeys = true;
91
if (
92
key.match(/content-encoding/i) ||
93
key.match(/transfer-encoding/i) ||
94
key.match(/content-length/i)
95
) {
96
delete responseHeaders[key];
97
}
98
}
99
if (cached.encoding) {
100
const key = isLowerKeys ? 'content-encoding' : 'Content-Encoding';
101
responseHeaders[key] = cached.encoding;
102
}
103
if (
104
cached.contentType &&
105
!responseHeaders['content-type'] &&
106
!responseHeaders['Content-Type']
107
) {
108
const key = isLowerKeys ? 'content-type' : 'Content-Type';
109
responseHeaders[key] = cached.contentType;
110
}
111
const lengthKey = isLowerKeys ? 'content-length' : 'Content-Length';
112
responseHeaders[lengthKey] = String(Buffer.byteLength(cached.file, 'utf8'));
113
this.shouldServeCachedData = true;
114
this.data.push(cached.file);
115
}
116
}
117
118