diff --git a/src/App.tsx b/src/App.tsx index e5d7869..a6b5400 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -5,7 +5,7 @@ import * as localforage from "localforage"; import Matrix from "./Matrix"; import Login from "./Login"; import Guide from "./Guide"; -import { LoginData } from "./types"; +import { LoginData, WellKnown } from "./types"; interface AppState { state: string | null; @@ -15,12 +15,14 @@ interface AppState { class App extends Component<{}, AppState> { private loginData: null | LoginData; + private well_known: null | WellKnown; private timeout: null | number; public state: AppState; constructor(props: {}) { super(props); this.loginData = null; + this.well_known = null; this.timeout = null; this.state = { state: null, @@ -35,6 +37,9 @@ class App extends Component<{}, AppState> { this.setState({ state: "login" }); } }); + localforage.getItem("well_known").then((well_known: unknown) => { + this.well_known = well_known as WellKnown; + }); localforage.getItem("guide").then((value: unknown) => { this.setState({ guide: Boolean(value) }); }); @@ -78,7 +83,7 @@ class App extends Component<{}, AppState> { } if (state === "matrix") { - return ; + return ; } alert("Some error occured. This must not happen"); window.close(); diff --git a/src/Login.tsx b/src/Login.tsx index 5ce6583..933b06b 100644 --- a/src/Login.tsx +++ b/src/Login.tsx @@ -1,11 +1,7 @@ import { Component } from "inferno"; -import { createClient } from "matrix-js-sdk"; -import * as localforage from "localforage"; import { TextListItem, TextInput, Header, SoftKey, ListViewKeyed } from "KaiUI"; -import shared from "./shared"; -import { fetch as customFetch } from "./fetch"; -import { WellKnown } from "./types"; import LoginWithQR from "./LoginWithQR"; +import LoginHandler from "./LoginHandler"; import { LoginFlow } from "matrix-js-sdk/lib/@types/auth"; interface LoginState { @@ -15,14 +11,13 @@ interface LoginState { } class Login extends Component<{}, LoginState> { - private homeserverUrl: string; - private homeserverName: string; - private loginFlows: LoginFlow[]; private readonly stageNames: string[]; private selectedLoginFlow?: LoginFlow; + public state: LoginState; + private loginHandler: LoginHandler; + private homeserverName: string; private username: string; private password: string; - public state: LoginState; cursorChangeCb = (cursor: number) => { this.setState({ cursor: cursor }); @@ -59,79 +54,18 @@ class Login extends Component<{}, LoginState> { } }; - doLogin = () => { - if (!this.selectedLoginFlow) { - throw new Error("selectedLoginFlow is not defined"); - } - switch (this.selectedLoginFlow.type) { - case "m.login.password": - shared.mClient - .loginWithPassword( - `@${this.username}:${this.homeserverName}`, - this.password - ) - .then((result: any) => { - localforage.setItem("login", result).then(() => { - alert("Logged in as " + this.username); - window.location = window.location; - }); - }) - .catch((err: any) => { - switch (err.errcode) { - case "M_FORBIDDEN": - alert("Incorrect login credentials"); - break; - case "M_USER_DEACTIVATED": - alert("This account has been deactivated"); - break; - case "M_LIMIT_EXCEEDED": - const retry = Math.ceil(err.retry_after_ms / 1000); - alert("Too many requests! Retry after" + retry.toString()); - break; - default: - alert("Login failed for some unknown reason"); - break; - } - }); - break; - default: - alert("Invalid/unsupported login method. This is likely a bug"); - break; - } - }; - rightCb = () => { switch (this.state.stage) { case 0: - this.homeserverName = this.homeserverName.replace("https://", ""); - this.homeserverName = this.homeserverName.replace("http://", ""); - fetch(`https://${this.homeserverName}/.well-known/matrix/client`) - .then((r: Response) => { - if (r.ok) { - r.json().then((j: WellKnown) => { - this.homeserverUrl = j["m.homeserver"].base_url; - shared.mClient = createClient({ - baseUrl: this.homeserverUrl, - fetchFn: customFetch, - }); - shared.mClient - .loginFlows() - .then((result) => { - this.loginFlows = result.flows; - this.setState({ cursor: 0, stage: 1 }); - }) - .catch((e: any) => console.log(e)); - }); - } else { - alert( - "Cannot connect to homeserver. Are you sure the address valid?" - ); - } - }) - .catch((e) => console.log(e)); + this.loginHandler.findHomeserver(this.homeserverName).then( () => { + this.setState({ cursor: 0, stage: 1}) + }).catch((e: any) => { + window.alert("Could not connect to homeserver"); + console.log(e); + }); break; case 1: - this.selectedLoginFlow = this.loginFlows[this.state.cursor]; + this.selectedLoginFlow = this.loginHandler.loginFlows[this.state.cursor]; if (this.selectedLoginFlow.type !== "m.login.password") { window.alert("The selected login method is not implemented, yet."); } else { @@ -139,7 +73,14 @@ class Login extends Component<{}, LoginState> { } break; case 2: - this.doLogin(); + let loginData = {'username': this.username, 'password': this.password}; + if (this.selectedLoginFlow !== undefined) { + this.loginHandler.doLogin(this.selectedLoginFlow, loginData).then(() => { + window.location = window.location; + }).catch((e) => alert(e.message)); + } else { + throw new Error("Undefined selectedLoginFlow") + } break; default: alert("Invalid stage!"); @@ -155,12 +96,11 @@ class Login extends Component<{}, LoginState> { constructor(props: any) { super(props); - this.stageNames = ["Login Info", "Login method", "Login"]; - this.loginFlows = []; + this.homeserverName = ""; this.username = ""; this.password = ""; - this.homeserverName = ""; - this.homeserverUrl = ""; + this.stageNames = ["Login Info", "Login method", "Login"]; + this.loginHandler = new LoginHandler() this.state = { stage: 0, cursor: 0, @@ -178,7 +118,8 @@ class Login extends Component<{}, LoginState> { render() { if (this.state.loginWithQr) { - return ; + return ; + // return ; } let listViewChildren; switch (this.state.stage) { @@ -204,7 +145,7 @@ class Login extends Component<{}, LoginState> { }, { tertiary: - "Press Call button and scan a QR in the following format to login with QR code instead of typing all these(PASS = password authentication): PASS server_name username password", + "Press Call button and scan a QR in the following format to login with QR code instead of typing all these (PASS = password authentication): PASS server_name username password", type: "text", key: "qrHint", }, @@ -214,7 +155,7 @@ class Login extends Component<{}, LoginState> { }); break; case 1: - listViewChildren = this.loginFlows.map((flow: LoginFlow) => { + listViewChildren = this.loginHandler.loginFlows.map((flow: LoginFlow) => { return ; }); break; diff --git a/src/LoginHandler.ts b/src/LoginHandler.ts new file mode 100644 index 0000000..38fb6c8 --- /dev/null +++ b/src/LoginHandler.ts @@ -0,0 +1,118 @@ +import { fetch as customFetch } from "./fetch"; +import { WellKnown } from "./types"; +import { LoginFlow } from "matrix-js-sdk/lib/@types/auth"; +import * as localforage from "localforage"; +import { createClient } from "matrix-js-sdk"; +import shared from "./shared"; + +export default class LoginHandler { + public base_url: string; + public homeserverName: string; + public username: string; + public password: string; + public loginFlows: LoginFlow[]; + + constructor() { + this.homeserverName = ""; + this.username = ""; + this.password = ""; + this.loginFlows = []; + this.base_url = ""; + } + + private setWellKnown(well_known: WellKnown) { + return localforage.setItem("well_known", well_known); + } + + public async doLogin(loginFlow: LoginFlow, loginData: any) { + // TODO implement more login flows + // Instead of implementing them one by one, consider using mClient.login + // and passing loginData (which needs to be properly formed according to their spec) + // (may or may not be a bad idea) + try { + let loginResult: any; + let username: string = `@${loginData.username}:${this.homeserverName}`; + switch (loginFlow.type) { + case "m.login.password": + let password: string = loginData.password; + loginResult = await shared.mClient + .loginWithPassword(username, password); + break; + default: + throw new Error("Unsupported"); + break; + } + if (loginResult.well_known) { + this.setWellKnown(loginResult.well_known) + console.log("Received a well_known from client login property. Updating previous settings.") + console.log(loginResult.well_known) + } + await localforage.setItem("login", loginResult); + alert("Logged in as " + username); + } catch (e: any) { + let message: string; + switch (e.errcode) { + case "M_FORBIDDEN": + message = "Incorrect login credentials"; + break; + case "M_USER_DEACTIVATED": + message = "This account has been deactivated"; + break; + case "M_LIMIT_EXCEEDED": + const retry = Math.ceil(e.retry_after_ms / 1000); + message = `Too many requests! Retry after ${retry.toString()}`; + break; + default: + if (e.message === "Unsupported") { + message = "Login flow selected is unsupported" + } else if (e.errcode) { + message = e.errcode; + } else { + message = `Login failed for some unknown reason: ${e.message}`; + } + break; + } + throw new Error(message) + } + } + + public async findHomeserver(name: string) { + name = name.replace("https://", ""); + name = name.replace("http://", ""); + this.homeserverName = name; + let base_url: string = ""; + let well_known_url = `https://${name}/.well-known/matrix/client`; + try { + let r: Response = await fetch(well_known_url); + if (!r.ok) { + throw new Error("404"); + + } + let well_known: WellKnown = await r.json(); + base_url = well_known["m.homeserver"].base_url; + } catch (e: any) { + console.log(`.well-known not found or malformed at ${well_known_url}`); + base_url = "https://" + name; + } finally { + this.base_url = base_url; + try { + shared.mClient = createClient({ + baseUrl: base_url, + fetchFn: customFetch, + }); + let result = await shared.mClient.loginFlows() + if (! result.flows) { + throw new Error("Got no flows"); + } + this.loginFlows = result.flows; + } catch (e) { + alert(`No server found at ${base_url}`) + console.log(e); + } + this.setWellKnown({ + "m.homeserver": {"base_url": base_url}, + "m.identity_server": {"base_url": "https://vector.im"}, // TODO Where to infer this outside of actual .well-known? + }) + } + } +} diff --git a/src/LoginWithQR/LoginWithQR.tsx b/src/LoginWithQR/LoginWithQR.tsx index b7ba8eb..f6ea2ae 100644 --- a/src/LoginWithQR/LoginWithQR.tsx +++ b/src/LoginWithQR/LoginWithQR.tsx @@ -1,16 +1,18 @@ import { Component } from "inferno"; import QrScanner from "qr-scanner"; -import { createClient } from "matrix-js-sdk"; -import * as localforage from "localforage"; +import { LoginFlow } from "matrix-js-sdk/lib/@types/auth"; import "./LoginWithQR.css"; import { SoftKey } from "KaiUI"; -import shared from "../shared"; -import { fetch as customFetch } from "../fetch"; -import { WellKnown } from "../types"; +import LoginHandler from "../LoginHandler"; -class LoginWithQR extends Component<{}, {}> { +interface LoginWithQRProps { + loginHandler: LoginHandler; +} + +class LoginWithQR extends Component { private video?: HTMLVideoElement; + private loginHandler: LoginHandler; startScanning = () => { if (!this.video) { @@ -27,104 +29,52 @@ class LoginWithQR extends Component<{}, {}> { scanner.start(); }; - doLogin = (data: string) => { + private login_flows_short: {[key: string]: string} = { + "PASS": "m.login.password" + } + + private async doLogin (data: string) { let decodedParts: string[] = data.split(" ", 4); - const flow = decodedParts[0]; + let flow = decodedParts[0]; const server_name = decodedParts[1]; const username = decodedParts[2]; - let password: string; - if ( - window.confirm( - `Do you confirm? Flow: ${flow} | Server name: ${server_name} | Username: ${username}` - ) - ) { - const start: number = - flow.length + server_name.length + username.length + 3; - password = data.substring(start); - fetch(`https://${server_name}/.well-known/matrix/client`).then( - (r: Response) => { - if (r.ok) { - r.json() - .then((j: WellKnown) => { - const server_url: string = j["m.homeserver"].base_url; - shared.mClient = createClient({ - baseUrl: server_url, - fetchFn: customFetch, - }); - shared.mClient - .loginFlows() - .then((result) => { - let gotPasswordLogin = false; - for (let flow of result.flows) { - if ("m.login.password" === flow.type) { - gotPasswordLogin = true; - break; - } - } - if (gotPasswordLogin) { - shared.mClient - .loginWithPassword( - `@${username}:${server_name}`, - password - ) - .then((result: any) => { - localforage.setItem("login", result).then(() => { - window.alert("Logged in as " + username); - window.location = window.location; - }); - }) - .catch((err: any) => { - switch (err.errcode) { - case "M_FORBIDDEN": - alert("Incorrect login credentials"); - break; - case "M_USER_DEACTIVATED": - alert("This account has been deactivated"); - break; - case "M_LIMIT_EXCEEDED": - const retry = Math.ceil( - err.retry_after_ms / 1000 - ); - alert( - "Too many requests! Retry after" + - retry.toString() - ); - break; - default: - alert("Login failed! Unknown reason"); - break; - } - // eslint-disable-next-line no-self-assign - window.location = window.location; - }); - } else { - window.alert( - "This homeserver does not support authentication with password" - ); - } - }) - .catch((e) => { - window.alert("Error getting login flows from the server"); - console.log(e); - }); - }) - .catch((e) => { - window.alert("Error getting information about the server"); - console.log("REPORT", e); - }); - } else { - alert( - "Cannot connect to homeserver. Are you sure the address is correct?" - ); + const start = flow.length + server_name.length + username.length + 3; + let password: string = data.substring(start); + // TODO implement more flows + if (window.confirm( + `Do you confirm? Flow: ${flow} | Server name: ${server_name} | Username: ${username}`)) { + // users can either write the full m.login.password (or whatever other flow) or use a shorthand + // This maps the shorthand to the actual flow identificator + if (!flow.startsWith("m.login")) { + flow = this.login_flows_short[flow]; + } + if (flow !== "m.login.password") { + alert("Password authentication is the only supported flow currently") + return; + } + try { + await this.loginHandler.findHomeserver(server_name); + let selected_flow: LoginFlow | undefined; + for (let available_flow of this.loginHandler.loginFlows) { + if (available_flow.type === flow) { + selected_flow = available_flow; } } - ); + if (selected_flow !== undefined) { + let loginData = {'username': username, 'password': password}; + await this.loginHandler.doLogin(selected_flow, loginData); + window.location = window.location; + } + } catch (e) { + alert(e) + } } }; constructor(props: any) { super(props); this.state = null; + this.loginHandler = props.loginHandler; } componentDidMount() { diff --git a/src/Matrix.tsx b/src/Matrix.tsx index 8348008..8790e5f 100644 --- a/src/Matrix.tsx +++ b/src/Matrix.tsx @@ -27,6 +27,7 @@ const vapidPublicKey = interface MatrixProps { data: any; + well_known: any; } interface Call { @@ -321,10 +322,10 @@ class Matrix extends Component { userId: props.data.user_id, accessToken: props.data.access_token, deviceId: props.data.device_id, - baseUrl: props.data.well_known["m.homeserver"].base_url, + baseUrl: props.well_known["m.homeserver"].base_url, identityServer: - props.data.well_known["m.identity_server"] && - props.data.well_known["m.identity_server"].base_url, + props.well_known["m.identity_server"] && + props.well_known["m.identity_server"].base_url, // store: new matrixcs.IndexedDBStore({ indexedDB: window.indexedDB, localStorage: window.localStorage }), }); const client = shared.mClient;