Skip to content

Commit

Permalink
[desktop] Fix making excessive handshakes
Browse files Browse the repository at this point in the history
Since we switched to built-in fetch() as our implementation we noticed
that a number of users are making excessive TLS handshakes. We've found
that by default undici (that's powering node's built-in fetch()) has
low idle timeout and has no default limit for connections.

We are now explicitly using undici (also more recent version) with
explicitly configured dispatcher to adjust the http client parameters.

We should switch to HTTP2 once it stabilizes in undici.

fix #6053
  • Loading branch information
charlag authored and ganthern committed Nov 13, 2023
1 parent 5f5418d commit 803bbf0
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 8 deletions.
20 changes: 20 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"qrcode-svg": "1.0.0",
"squire-rte": "2.2.2",
"systemjs": "6.10.2",
"undici": "5.27.2",
"winreg": "1.2.4"
},
"devDependencies": {
Expand Down
34 changes: 26 additions & 8 deletions src/desktop/net/ProtocolProxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,46 @@ import { Session } from "electron"
import { errorToObj } from "../../api/common/threading/MessageDispatcher.js"
import { lazyMemoized } from "@tutao/tutanota-utils"
import { getMimeTypeForFile } from "../files/DesktopFileFacade.js"
import { ServiceUnavailableError } from "../../api/common/error/RestError.js"
import { Agent, fetch, RequestInfo as UndiciRequestInfo, RequestInit as UndiciRequestInit } from "undici"

type GlobalFetch = typeof global.fetch

const TAG = "[ProtocolProxy]"

export const ASSET_PROTOCOL = "asset"
const PROXIED_REQUEST_READ_TIMEOUT = 20000

/** How long the socket should stay open without any data sent over it. See IDLE_TIMEOUT_MS in tutadb. */
const SOCKET_IDLE_TIMEOUT_MS = 5 * 60 * 10_000 + 1000
/** Timeout between reading data. */
const READ_TIMEOUT_MS = 20_000

/**
* intercept & proxy https, http and asset requests on a session
* @param session the webContents session we want to intercept requests on
* @param assetDir the base directory of allowable scripts, images and other resources that the app may load.
*/
export function handleProtocols(session: Session, assetDir: string): void {
doHandleProtocols(session, assetDir, fetch, path, fs)
// We do not enable HTTP2 yet because it is still experimental (and buggy).
const agent = new Agent({
connections: 3,
keepAliveTimeout: SOCKET_IDLE_TIMEOUT_MS,
bodyTimeout: READ_TIMEOUT_MS,
headersTimeout: READ_TIMEOUT_MS,
})
const customFetch: typeof fetch = (info: UndiciRequestInfo, requestInit?: UndiciRequestInit) => {
return fetch(info, {
...(requestInit ?? {}),
dispatcher: agent,
})
}
// It's a little crime to say that our fetch is like builtin fetch but it actually is, it's just TS is a bit uncooperative.
doHandleProtocols(session, assetDir, customFetch as GlobalFetch, path, fs)
}

/**
* exported for testing
*/
export function doHandleProtocols(session: Session, assetDir: string, fetchImpl: typeof fetch, pathModule: typeof path, fsModule: typeof fs): void {
export function doHandleProtocols(session: Session, assetDir: string, fetchImpl: GlobalFetch, pathModule: typeof path, fsModule: typeof fs): void {
if (!interceptProtocol("http", session, fetchImpl)) throw new Error("could not intercept http protocol")
if (!interceptProtocol("https", session, fetchImpl)) throw new Error("could not intercept https protocol")
if (!handleAssetProtocol(session, assetDir, pathModule, fsModule)) throw new Error("could not register asset protocol")
Expand All @@ -37,12 +57,11 @@ export function doHandleProtocols(session: Session, assetDir: string, fetchImpl:
* @param protocol http and https use different modules, so we need to intercept them separately.
* @param fetchImpl an implementation of the fetch API (Request) => Promise<Response>
*/
function interceptProtocol(protocol: string, session: Session, fetchImpl: typeof fetch): boolean {
function interceptProtocol(protocol: string, session: Session, fetchImpl: GlobalFetch): boolean {
if (session.protocol.isProtocolHandled(protocol)) return true
session.protocol.handle(protocol, async (request: Request): Promise<Response> => {
session.protocol.handle(protocol, async (request: GlobalRequest): Promise<Response> => {
const { method, url, headers } = request
const startTime: number = Date.now()

if (!url.startsWith(protocol)) {
return new Response(null, { status: 400 })
} else if (method == "OPTIONS") {
Expand All @@ -56,7 +75,6 @@ function interceptProtocol(protocol: string, session: Session, fetchImpl: typeof
headers,
method,
keepalive: true,
signal: AbortSignal.timeout(PROXIED_REQUEST_READ_TIMEOUT),
}
const body = await request.arrayBuffer()
if (body.byteLength > 0) {
Expand Down

0 comments on commit 803bbf0

Please sign in to comment.