Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions src/api/baseclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { APIError } from "./error"
export interface ExtraParams {
signal?: AbortSignal
pathPrefix?: string
headers?: Record<string, string>
query?: Record<string, string | undefined>
}

export class BaseAPIClient {
Expand Down Expand Up @@ -60,11 +62,12 @@ export class BaseAPIClient {
extraParams?: ExtraParams,
): Promise<ResponseType> {
if (method === "GET" || method === "HEAD") {
reqData = extraParams
extraParams = reqData as ExtraParams
reqData = undefined
}
const reqParams: RequestInit & { headers: Record<string, string> } = {
method,
headers: {},
headers: { ...extraParams?.headers },
signal: extraParams?.signal,
}
const token = await this.getToken()
Expand All @@ -83,6 +86,13 @@ 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)) {
if (value) {
url.searchParams.set(key, value)
}
}
}
const resp = await fetch(url.toString(), reqParams)
let respData
try {
Expand Down
35 changes: 34 additions & 1 deletion src/api/loginclient.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -144,6 +156,27 @@ 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)
}
Expand Down
4 changes: 3 additions & 1 deletion src/api/provisionclient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,9 @@ export class ProvisioningClient extends BaseAPIClient {

async startLogin(flowID: string, refresh: () => void): Promise<LoginClient> {
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)
}
Expand Down
7 changes: 7 additions & 0 deletions src/app/BridgeLoginView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,13 @@ const LoginStep = ({ step, onSubmit, onLoginComplete, onCancel, cableState, cabl
<button className="cancel-button" onClick={onCancel}>Cancel</button>
</div>
</div>
case "client_http":
return <div className="login-form type-client-http">
Executing client-side HTTP request...
<div className="login-form-buttons">
<button className="cancel-button" onClick={onCancel}>Cancel</button>
</div>
</div>
case "complete":
return <div className="login-form type-complete">
<button className="close-button" onClick={onLoginComplete}>Close</button>
Expand Down
15 changes: 15 additions & 0 deletions src/electron.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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<typeof fetch>[1],
): Promise<LoginClientHTTPResponse> => {
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) {
Expand Down
44 changes: 26 additions & 18 deletions src/preload.ts
Original file line number Diff line number Diff line change
@@ -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<void>
doWebAuthn: (params: LoginWebAuthnParams) => Promise<AuthenticationResponseJSON>
doClientHTTP: (...params: Parameters<typeof fetch>) => Promise<LoginClientHTTPResponse>,
accessTokenChanged: (newDetails: AccessTokenChangedParams) => Promise<void>
openInBrowser: (url: string) => Promise<void>
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<void>
doWebAuthn: (params: LoginWebAuthnParams) => Promise<AuthenticationResponseJSON>
accessTokenChanged: (newDetails: AccessTokenChangedParams) => Promise<void>
openInBrowser: (url: string) => Promise<void>
isDevBuild: boolean,
}
mautrixAPI: MautrixAPI
}
}

Expand Down
24 changes: 24 additions & 0 deletions src/types/loginstep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ export type LoginStepData =
LoginStepDisplayAndWait |
LoginStepCookies |
LoginStepWebAuthn |
LoginStepClientHTTP |
LoginStepComplete

interface baseLoginStep {
step_id: string
txn_id?: string
instructions?: string
}

Expand Down Expand Up @@ -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<string, string[]>
body?: string
}

export type LoginClientHTTPResponse = {
status_code: number
final_url?: string
headers?: Record<string, string[]>
body?: string
} | {
error: string
}

export interface LoginWebAuthnParams {
url: string
publicKey: PublicKeyCredentialRequestOptionsJSON
Expand Down