Skip to content
This repository was archived by the owner on May 30, 2025. It is now read-only.

Commit 438df6b

Browse files
authored
feat: support client certificates in fetch (#222)
fetch client certs + http agent per runtime
1 parent 83d386b commit 438df6b

File tree

6 files changed

+254
-32
lines changed

6 files changed

+254
-32
lines changed

packages/core/src/bridge/fetch.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const parseURL = process.env.FLY_ENV === "test" ? parseUrlWithRemapping : parseU
1515
registerBridge(
1616
"fetch",
1717
(rt: Runtime, bridge: Bridge, url: string, init: RequestInit, body: FetchBody, cb: IvmCallback) => {
18-
log.debug("native fetch with url:", url)
18+
log.debug("native fetch", { url })
1919

2020
const parsedUrl = parseURL(url)
2121
const handler = getRequestHandler(parsedUrl)

packages/core/src/bridge/fetch/http.ts

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,6 @@ import { RequestInit, URL, FetchBody, ResponseInit } from "./types"
1111
export const httpProtocol = "http:"
1212
export const httpsProtocol = "https:"
1313

14-
const connectionStats = {
15-
created: 0,
16-
current: 0,
17-
reused: 0,
18-
keptAlive: 0
19-
}
20-
2114
export function handleRequest(
2215
rt: Runtime,
2316
bridge: Bridge,
@@ -28,7 +21,7 @@ export function handleRequest(
2821
return new Promise((resolve, reject) => {
2922
try {
3023
const httpFn = url.protocol === httpProtocol ? http.request : https.request
31-
const httpAgent = url.protocol === httpProtocol ? fetchAgent : fetchHttpsAgent
24+
const httpAgent = getAgent(rt, url.protocol)
3225

3326
let req: http.ClientRequest
3427
let timeout: NodeJS.Timer | undefined
@@ -40,7 +33,7 @@ export function handleRequest(
4033
const reqOptions: https.RequestOptions = {
4134
agent: httpAgent,
4235
protocol: url.protocol,
43-
path: url.pathname + url.search, // should this include the hash fragment?
36+
path: url.pathname + url.search,
4437
hostname: url.hostname,
4538
host: url.host,
4639
port: url.port,
@@ -49,6 +42,14 @@ export function handleRequest(
4942
timeout: 60 * 1000
5043
}
5144

45+
if (init.certificate) {
46+
reqOptions.key = init.certificate.key
47+
reqOptions.cert = init.certificate.cert
48+
reqOptions.pfx = init.certificate.pfx
49+
reqOptions.ca = init.certificate.ca
50+
reqOptions.passphrase = init.certificate.passphrase
51+
}
52+
5253
if (httpFn === https.request) {
5354
reqOptions.servername = reqOptions.hostname
5455
}
@@ -150,6 +151,13 @@ export function handleRequest(
150151
})
151152
}
152153

154+
const connectionStats = {
155+
created: 0,
156+
current: 0,
157+
reused: 0,
158+
keptAlive: 0
159+
}
160+
153161
// this keeps track of connection counts for the fetch agent
154162
function createMonitoredAgent<T extends http.Agent>(agent: T) {
155163
const a: any = agent
@@ -175,33 +183,64 @@ function createMonitoredAgent<T extends http.Agent>(agent: T) {
175183
})
176184
}
177185

178-
// tslint:disable-next-line
179-
let maxSockets = parseInt(process.env.MAX_FETCH_SOCKETS || "")
186+
let maxSockets = parseInt(process.env.MAX_FETCH_SOCKETS || "", 10)
180187
if (isNaN(maxSockets) || maxSockets < 1) {
181188
maxSockets = 1024 // maybe sane limit
182189
}
183-
// tslint:disable-next-line
184-
let maxFreeSockets = parseInt(process.env.MAX_FETCH_FREE_SOCKETS || "")
190+
191+
let maxFreeSockets = parseInt(process.env.MAX_FETCH_FREE_SOCKETS || "", 10)
185192
if (isNaN(maxFreeSockets) || maxFreeSockets < 1) {
186193
maxFreeSockets = 256 // default
187194
}
188195

189196
log.debug("Fetch connection pool:", { maxSockets, maxFreeSockets })
190197

191-
const fetchAgent = createMonitoredAgent(
192-
new http.Agent({
193-
keepAlive: true,
194-
keepAliveMsecs: 5 * 1000,
195-
maxSockets,
196-
maxFreeSockets
197-
})
198-
)
199-
const fetchHttpsAgent = createMonitoredAgent(
200-
new https.Agent({
201-
keepAlive: true,
202-
keepAliveMsecs: 1000,
203-
rejectUnauthorized: false, // for simplicity
204-
maxSockets,
205-
maxFreeSockets
206-
})
207-
)
198+
const runtimeAgents = new Map<string, http.Agent>()
199+
200+
function getAgent(rt: Runtime, protocol: string): http.Agent {
201+
const key = `${rt.app.id}:${protocol}`
202+
let agent = runtimeAgents.get(key)
203+
204+
if (!agent) {
205+
if (protocol === "http:") {
206+
agent = createMonitoredAgent(
207+
new http.Agent({
208+
keepAlive: true,
209+
keepAliveMsecs: 1000,
210+
maxSockets,
211+
maxFreeSockets
212+
})
213+
)
214+
} else if (protocol === "https:") {
215+
agent = createMonitoredAgent(
216+
new https.Agent({
217+
keepAlive: true,
218+
keepAliveMsecs: 1000,
219+
rejectUnauthorized: false,
220+
maxSockets,
221+
maxFreeSockets
222+
})
223+
)
224+
} else {
225+
throw new Error("Unsupported protocol " + protocol)
226+
}
227+
228+
runtimeAgents.set(key, agent)
229+
}
230+
231+
return agent
232+
}
233+
234+
function destroyIdleAgents() {
235+
for (const [name, agent] of runtimeAgents) {
236+
const sockets = Object.keys(agent.sockets).length
237+
const requests = Object.keys(agent.requests).length
238+
if (sockets === 0 && requests === 0) {
239+
log.debug("destroying http agent pool", name)
240+
agent.destroy()
241+
runtimeAgents.delete(name)
242+
}
243+
}
244+
}
245+
246+
setInterval(destroyIdleAgents, 30000).unref()

packages/core/src/bridge/fetch/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export interface RequestInit {
77
timeout?: number
88
readTimeout?: number
99
headers?: Record<string, string>
10+
certificate?: Certificate
1011
}
1112

1213
export type FetchBody = string | number | ArrayBuffer | Buffer | null
@@ -17,3 +18,11 @@ export interface ResponseInit {
1718
headers?: Record<string, string>
1819
body?: number | string
1920
}
21+
22+
export interface Certificate {
23+
key?: string | Buffer | Array<string | Buffer>
24+
cert?: string | Buffer | Array<string | Buffer>
25+
ca?: string | Buffer | Array<string | Buffer>
26+
pfx?: string | Buffer | Array<string | Buffer>
27+
passphrase?: string
28+
}

packages/v8env/src/fetch.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,13 @@ declare var bridge: any
99
export interface FlyRequestInit extends RequestInit {
1010
timeout?: number
1111
readTimeout?: number
12+
certificate?: {
13+
key?: string | Buffer | Array<string | Buffer>
14+
cert?: string | Buffer | Array<string | Buffer>
15+
ca?: string | Buffer | Array<string | Buffer>
16+
pfx?: string | Buffer | Array<string | Buffer>
17+
passphrase?: string
18+
}
1219
}
1320

1421
/**
@@ -32,7 +39,8 @@ export function fetch(req: RequestInfo, init?: FlyRequestInit): Promise<Response
3239
method: req.method,
3340
headers: (req.headers && req.headers.toJSON()) || {},
3441
timeout: init && init.timeout,
35-
readTimeout: (init && init.readTimeout) || 30 * 1000
42+
readTimeout: (init && init.readTimeout) || 30 * 1000,
43+
certificate: init && init.certificate
3644
}
3745
if (!req.bodySource) {
3846
bridge.dispatch("fetch", url, init, null, fetchCb)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
fly.http.respondWith(async req => {
2+
const certificate = await req.json()
3+
4+
try {
5+
return fetch("https://server.cryptomix.com/secure/", {
6+
certificate
7+
})
8+
} catch (err) {
9+
return new Response("Fetch error: " + err, { status: 500 })
10+
}
11+
})
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
import * as path from "path"
2+
3+
setupApps({
4+
"edge.local": path.resolve(__dirname, "client-certs.edge.js")
5+
})
6+
7+
test("client certs pass to server", async () => {
8+
const response = await fetch("http://edge.local", {
9+
method: "POST",
10+
headers: {
11+
"content-type": "application/json"
12+
},
13+
body: JSON.stringify({
14+
cert,
15+
key,
16+
ca: [ca]
17+
})
18+
})
19+
expect(response.status).toEqual(200)
20+
expect(await response.text()).toMatch(/SSL Authentication OK!/)
21+
})
22+
23+
test("throws on an invalid cert+key", async () => {
24+
const response = await fetch("http://edge.local", {
25+
method: "POST",
26+
headers: {
27+
"content-type": "application/json"
28+
},
29+
body: JSON.stringify({
30+
cert,
31+
key: "bad-key",
32+
ca: [ca]
33+
})
34+
})
35+
expect(response.status).toEqual(500)
36+
expect(await response.text()).toMatch(/no start line/)
37+
})
38+
39+
const ca = `-----BEGIN CERTIFICATE-----
40+
MIIFgDCCA2gCCQCLtoAQKfdgizANBgkqhkiG9w0BAQsFADCBgTELMAkGA1UEBhMC
41+
VVMxCzAJBgNVBAgMAk1BMQ8wDQYDVQQHDAZCb3N0b24xEzARBgNVBAoMCkV4YW1w
42+
bGUgQ28xEDAOBgNVBAsMB3RlY2hvcHMxCzAJBgNVBAMMAmNhMSAwHgYJKoZIhvcN
43+
AQkBFhFjZXJ0c0BleGFtcGxlLmNvbTAeFw0xOTAzMTIyMTEzNTVaFw00NjA3Mjcy
44+
MTEzNTVaMIGBMQswCQYDVQQGEwJVUzELMAkGA1UECAwCTUExDzANBgNVBAcMBkJv
45+
c3RvbjETMBEGA1UECgwKRXhhbXBsZSBDbzEQMA4GA1UECwwHdGVjaG9wczELMAkG
46+
A1UEAwwCY2ExIDAeBgkqhkiG9w0BCQEWEWNlcnRzQGV4YW1wbGUuY29tMIICIjAN
47+
BgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA6hn1srGs0pLbxECr4dmx5rspVAtH
48+
a8+v06ruhov3QkiaLnPSRUBTsvDPyOsipyzk0lU5LToYIlG7jNDmwt4nguVVGmAz
49+
5V1QV7uHiJgIImyDbekQbcwW7bVi1dcp5RwD8zfCJqD/v0/TTtNBW4vMDKN8DR2h
50+
Co9rd1XFnOTSzaC9H3I/bHq2+UR/v7oKW4IweIaEqs7aoG8O/qXpd8a0h9lAoTQ5
51+
24kjRmRp/L+nDyTSzPD7ZK+WQ2JC/hKmV0RPTGDKP4at8mLKS3av9TBh6WLQZgWc
52+
svbBwSqamcsTI/iFoOYykRsMtM7cZ8FEFgjKnty4n7Y04TFbu0XJsWjYkF1kanm1
53+
QwoYFXpxwTmDKqf5RselxxDoWggygZqzMHUKR8cirJe+HYItF9qnD1+WQe1Kvx6c
54+
WCp5vw/Uzg71VobzSedyAxcN79jazyTH1CqNbRqrh/x0Dbg7qYQ3DmW/AvihiX3o
55+
2IKiASKiyFM+p2/C52OZY+JnDGKpbUkJ34mbBthJHPv9K0UxzMBxgK2EAifQblbk
56+
fPTZdPJixE08spDzHaBrhPkLGdqLWKm3ZQVXmS3/p5o0OA8izwdbPLFCRC/DoD20
57+
xLAXna7jJvgFWD2fSZ+YPLAlxIEpM8fg9jrO2nu29iUn2SRJWsCRB97RZtAx4M7t
58+
TRIcLQ64hfTztLMCAwEAATANBgkqhkiG9w0BAQsFAAOCAgEAUjr/6jqLfUfP4HYu
59+
8PtNd/bHqGGXo1VIdS2ne/1G3S3b3IHR/NVFedYfktx5TsMTCGJU+q1pG+PyBH7z
60+
jTgTkOac7J4knBZ5DhGxz3MObD57LpPq1Mtxhsgr9dKHW1eWQnFlE7oLnAqictDs
61+
Pa0tS+H5/YE6g4Ue2ZFr2ksMjjbmhDecrXhoZUWS3b/zl3JNQ/hczw+UNzKuK1AZ
62+
up+cqe8xKwmTfGxx8iwN2SlCjEAtNu1ioU+PLUi3LRyiAxlbm0ampMGNli0me8BQ
63+
N3QvFear3gVleTOCXcDOzYlRX76Ied7IXH7xkdEcDUtra5UaLDV/xzOxJOozhlCY
64+
sKO6fgbVS5gf2gVt3mLVRnVyevsl8P94HhjRsahzmy2tJGD8dCx62EyHyTyscZqX
65+
p/a+PMoeBb82AHllqcGspqxjv24TIsshA65LZbbDtGxJ1qRqMuKVAkOfTcI+u/1t
66+
H2OPe8G3F8HX6aCIm4hgKuRXQ5P28PmFNWCQhsyQR/dPQvVMvyNyKJY53yzzByrj
67+
42IRtl+MYCPQctvsC3Z/2Vfc/xf9Ujpz1OpkMJhmaFw+4PAh1Nbtf9YNdvwopvh7
68+
+IG7qjtHvG25zA8ogVoA4gRYk/qdfPQgX8twRBJAiBrlXtRVzlaM7mpWn0+QUzTF
69+
jNJLqa8fKQgNFs664HWb2od7vZ0=
70+
-----END CERTIFICATE-----`
71+
72+
const cert = `-----BEGIN CERTIFICATE-----
73+
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
103+
-----END CERTIFICATE-----`
104+
105+
const key = `-----BEGIN RSA PRIVATE KEY-----
106+
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
155+
-----END RSA PRIVATE KEY-----`

0 commit comments

Comments
 (0)