From cee16214e5ae917ba3aca02180641b2dcc35f784 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 3 Aug 2026 20:39:59 +0300 Subject: [PATCH 1/2] Add support for client_http step type --- src/api/baseclient.ts | 12 ++++++++-- src/api/loginclient.ts | 32 ++++++++++++++++++++++++++- src/api/provisionclient.ts | 4 +++- src/app/BridgeLoginView.tsx | 7 ++++++ src/electron.ts | 15 +++++++++++++ src/preload.ts | 44 ++++++++++++++++++++++--------------- src/types/loginstep.ts | 24 ++++++++++++++++++++ 7 files changed, 116 insertions(+), 22 deletions(-) diff --git a/src/api/baseclient.ts b/src/api/baseclient.ts index e8f2c27..4057816 100644 --- a/src/api/baseclient.ts +++ b/src/api/baseclient.ts @@ -3,6 +3,8 @@ import { APIError } from "./error" export interface ExtraParams { signal?: AbortSignal pathPrefix?: string + headers?: Record + query?: Record } export class BaseAPIClient { @@ -60,11 +62,12 @@ export class BaseAPIClient { extraParams?: ExtraParams, ): Promise { if (method === "GET" || method === "HEAD") { - reqData = extraParams + extraParams = reqData as ExtraParams + reqData = undefined } const reqParams: RequestInit & { headers: Record } = { method, - headers: {}, + headers: { ...extraParams?.headers }, signal: extraParams?.signal, } const token = await this.getToken() @@ -83,6 +86,11 @@ export class BaseAPIClient { if (this.loginID && !path.startsWith("/v3/login/")) { url.searchParams.set("login_id", this.loginID) } + if (extraParams?.query) { + for (const [key, value] of Object.entries(extraParams.query)) { + url.searchParams.set(key, value) + } + } const resp = await fetch(url.toString(), reqParams) let respData try { diff --git a/src/api/loginclient.ts b/src/api/loginclient.ts index bee421b..e878da4 100644 --- a/src/api/loginclient.ts +++ b/src/api/loginclient.ts @@ -1,5 +1,5 @@ import type { RespSubmitLogin } from "../types/login" -import type { LoginStepData } from "../types/loginstep" +import type { LoginClientHTTPResponse, LoginStepData } from "../types/loginstep" import type { ProvisioningClient } from "./provisionclient" const baseChromeUserAgent = window.navigator.userAgent @@ -105,6 +105,18 @@ export class LoginClient { }) } else if (this.#step.type === "display_and_wait") { this.wait() + } else if (this.#step.type === "client_http") { + const headers = Object.fromEntries(Object.entries(this.#step.client_http.headers ?? {}) + .map(([key, values]) => [key, values[0]])) + const body = this.#step.client_http.body ? Uint8Array.fromBase64(this.#step.client_http.body) : undefined + window.mautrixAPI.doClientHTTP(this.#step.client_http.url, { + method: this.#step.client_http.method, + headers, + body, + }).then( + resp => this.submitClientHTTP(resp), + err => this.submitClientHTTP({ error: err.toString() }), + ) } else if (this.#step.type === "complete") { setTimeout(this.onCompleteRefresh, 200) } @@ -144,6 +156,24 @@ export class LoginClient { "POST", `/v3/login/step/${this.loginID}/${this.#step.step_id}/${this.#step.type}`, params, + { signal: this.abortController.signal, query: { txn_id: this.#step.txn_id } }, + ).then(this.onStep, this.onError) + } + + private submitClientHTTP(resp: LoginClientHTTPResponse) { + if (this.abortController.signal.aborted) { + throw new Error("Login was cancelled") + } else if (this.submitInProgress) { + throw new Error("Cannot submit multiple steps concurrently") + } else if (this.#step.type !== "client_http") { + throw new Error("Mismatching step type for submitClientHTTP call") + } + console.log("Submitting client HTTP step", this.#step.step_id) + this.onLoading(true) + this.client.request( + "POST", + `/v3/login/client_http/${this.loginID}/${this.#step.txn_id}/${this.#step.client_http.request_id}`, + resp, { signal: this.abortController.signal }, ).then(this.onStep, this.onError) } diff --git a/src/api/provisionclient.ts b/src/api/provisionclient.ts index c3c708e..73dff29 100644 --- a/src/api/provisionclient.ts +++ b/src/api/provisionclient.ts @@ -42,7 +42,9 @@ export class ProvisioningClient extends BaseAPIClient { async startLogin(flowID: string, refresh: () => void): Promise { const resp: RespSubmitLogin = await this.request( - "POST", `/v3/login/start/${encodeURIComponent(flowID)}`, {}, + "POST", `/v3/login/start/${encodeURIComponent(flowID)}`, {}, { + query: { client_http: "1" }, + }, ) return new LoginClient(this, resp, refresh) } diff --git a/src/app/BridgeLoginView.tsx b/src/app/BridgeLoginView.tsx index 3ed01cd..9c4d885 100644 --- a/src/app/BridgeLoginView.tsx +++ b/src/app/BridgeLoginView.tsx @@ -124,6 +124,13 @@ const LoginStep = ({ step, onSubmit, onLoginComplete, onCancel, cableState, cabl + case "client_http": + return
+ Executing client-side HTTP request... +
+ +
+
case "complete": return
diff --git a/src/electron.ts b/src/electron.ts index e42bfeb..827f056 100644 --- a/src/electron.ts +++ b/src/electron.ts @@ -2,6 +2,7 @@ import path from "path" import { BrowserWindow, app, ipcMain, shell } from "electron" import started from "electron-squirrel-startup" import type { AccessTokenChangedParams } from "./preload" +import type { LoginClientHTTPResponse } from "./types/loginstep.ts" import { getSearch } from "./util/urlParse" import "./webauthn.ts" import "./webview.ts" @@ -26,6 +27,20 @@ ipcMain.handle("mautrix:open-in-browser", (_event, url: string) => { return shell.openExternal(url) }) +ipcMain.handle("mautrix:client-http", async ( + _event, url: string, init: Parameters[1], +): Promise => { + console.log("Sending client HTTP request", init?.method, url) + const resp = await fetch(url, init) + console.log("Got response", resp.status, "to", url) + return { + status_code: resp.status, + headers: Object.fromEntries(resp.headers.entries().map(([key, value]) => [key, [value]])), + body: new Uint8Array(await resp.arrayBuffer()).toBase64(), + final_url: resp.url, + } +}) + let mainWindow: BrowserWindow | undefined function loadIndexPage(search?: string) { diff --git a/src/preload.ts b/src/preload.ts index 7164656..d5bfde2 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -1,33 +1,41 @@ import { contextBridge, ipcRenderer } from "electron" -import type { LoginCookieOutput, LoginCookiesParams, LoginWebAuthnParams } from "./types/loginstep" +import type { + LoginClientHTTPResponse, + LoginCookieOutput, + LoginCookiesParams, + LoginWebAuthnParams, +} from "./types/loginstep" export interface AccessTokenChangedParams { homeserverURL: string accessToken: string } -contextBridge.exposeInMainWorld("mautrixAPI", { - openWebview: (params: LoginCookiesParams) => - ipcRenderer.invoke("mautrix:open-webview", params), +interface MautrixAPI { + openWebview: (params: LoginCookiesParams) => Promise<{ cookies: LoginCookieOutput }> + closeWebview: () => Promise + doWebAuthn: (params: LoginWebAuthnParams) => Promise + doClientHTTP: (...params: Parameters) => Promise, + accessTokenChanged: (newDetails: AccessTokenChangedParams) => Promise + openInBrowser: (url: string) => Promise + isDevBuild: boolean, +} + +const api: MautrixAPI = { + openWebview: params => ipcRenderer.invoke("mautrix:open-webview", params), closeWebview: () => ipcRenderer.invoke("mautrix:close-webview"), - doWebAuthn: (params: LoginWebAuthnParams) => - ipcRenderer.invoke("mautrix:webauthn", params), - accessTokenChanged: (newDetails: AccessTokenChangedParams) => - ipcRenderer.invoke("mautrix:access-token-changed", newDetails), - openInBrowser: (url: string) => ipcRenderer.invoke("mautrix:open-in-browser", url), + doWebAuthn: params => ipcRenderer.invoke("mautrix:webauthn", params), + doClientHTTP: (url, init) => ipcRenderer.invoke("mautrix:client-http", url, init), + accessTokenChanged: newDetails => ipcRenderer.invoke("mautrix:access-token-changed", newDetails), + openInBrowser: url => ipcRenderer.invoke("mautrix:open-in-browser", url), isDevBuild: process.env.NODE_ENV === "development", -}) +} + +contextBridge.exposeInMainWorld("mautrixAPI", api) declare global { interface Window { - mautrixAPI: { - openWebview: (params: LoginCookiesParams) => Promise<{ cookies: LoginCookieOutput }> - closeWebview: () => Promise - doWebAuthn: (params: LoginWebAuthnParams) => Promise - accessTokenChanged: (newDetails: AccessTokenChangedParams) => Promise - openInBrowser: (url: string) => Promise - isDevBuild: boolean, - } + mautrixAPI: MautrixAPI } } diff --git a/src/types/loginstep.ts b/src/types/loginstep.ts index 6cbe907..651cec4 100644 --- a/src/types/loginstep.ts +++ b/src/types/loginstep.ts @@ -7,10 +7,12 @@ export type LoginStepData = LoginStepDisplayAndWait | LoginStepCookies | LoginStepWebAuthn | + LoginStepClientHTTP | LoginStepComplete interface baseLoginStep { step_id: string + txn_id?: string instructions?: string } @@ -68,6 +70,28 @@ export type LoginStepWebAuthn = baseLoginStep & { webauthn: LoginWebAuthnParams, } +export type LoginStepClientHTTP = baseLoginStep & { + type: "client_http", + client_http: LoginClientHTTPParams, +} + +export interface LoginClientHTTPParams { + request_id: string + method: string + url: string + headers?: Record + body?: string +} + +export type LoginClientHTTPResponse = { + status_code: number + final_url?: string + headers?: Record + body?: string +} | { + error: string +} + export interface LoginWebAuthnParams { url: string publicKey: PublicKeyCredentialRequestOptionsJSON From 734e77392ed65092b7ee0cb1fd782b62dc79c886 Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Mon, 3 Aug 2026 21:40:40 +0300 Subject: [PATCH 2/2] Fix lint --- src/api/baseclient.ts | 6 ++++-- src/api/loginclient.ts | 5 ++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/api/baseclient.ts b/src/api/baseclient.ts index 4057816..057ff08 100644 --- a/src/api/baseclient.ts +++ b/src/api/baseclient.ts @@ -4,7 +4,7 @@ export interface ExtraParams { signal?: AbortSignal pathPrefix?: string headers?: Record - query?: Record + query?: Record } export class BaseAPIClient { @@ -88,7 +88,9 @@ export class BaseAPIClient { } if (extraParams?.query) { for (const [key, value] of Object.entries(extraParams.query)) { - url.searchParams.set(key, value) + if (value) { + url.searchParams.set(key, value) + } } } const resp = await fetch(url.toString(), reqParams) diff --git a/src/api/loginclient.ts b/src/api/loginclient.ts index e878da4..96b490b 100644 --- a/src/api/loginclient.ts +++ b/src/api/loginclient.ts @@ -156,7 +156,10 @@ export class LoginClient { "POST", `/v3/login/step/${this.loginID}/${this.#step.step_id}/${this.#step.type}`, params, - { signal: this.abortController.signal, query: { txn_id: this.#step.txn_id } }, + { + signal: this.abortController.signal, + query: { txn_id: this.#step.txn_id }, + }, ).then(this.onStep, this.onError) }