diff --git a/src/components/LoginPage.js b/src/components/LoginPage.js index 8585f69..dd8bdc9 100644 --- a/src/components/LoginPage.js +++ b/src/components/LoginPage.js @@ -1,6 +1,5 @@ import React, { useState, useEffect } from "react"; import { - fetchUtils, Form, FormDataConsumer, Notification, @@ -27,6 +26,13 @@ import { } from "@mui/material"; import { styled } from "@mui/material/styles"; import LockIcon from "@mui/icons-material/Lock"; +import { + getServerVersion, + getSupportedLoginFlows, + getWellKnownUrl, + isValidBaseUrl, + splitMxid, +} from "../synapse/synapse"; const FormBox = styled(Box)(({ theme }) => ({ display: "flex", @@ -170,87 +176,42 @@ const LoginPage = () => { window.location.href = ssoFullUrl; }; - const extractHomeServer = username => { - const usernameRegex = /@[a-zA-Z0-9._=\-/]+:([a-zA-Z0-9\-.]+\.[a-zA-Z]+)/; - if (!username) return null; - const res = username.match(usernameRegex); - if (res) return res[1]; - return null; - }; - const UserData = ({ formData }) => { const form = useFormContext(); const [serverVersion, setServerVersion] = useState(""); const handleUsernameChange = _ => { if (formData.base_url || cfg_base_url) return; - // check if username is a full qualified userId then set base_url accordially - const home_server = extractHomeServer(formData.username); - const wellKnownUrl = `https://${home_server}/.well-known/matrix/client`; - if (home_server) { - // fetch .well-known entry to get base_url - fetchUtils - .fetchJson(wellKnownUrl, { method: "GET" }) - .then(({ json }) => { - form.setValue("base_url", json["m.homeserver"].base_url); - }) - .catch(_ => { - // if there is no .well-known entry, try the home server name - form.setValue("base_url", `https://${home_server}`); - }); + // check if username is a full qualified userId then set base_url accordingly + const domain = splitMxid(formData.username)?.domain; + if (domain) { + getWellKnownUrl(domain).then(url => form.setValue("base_url", url)); } }; - useEffect( - _ => { - if ( - !formData.base_url || - !formData.base_url.match( - /^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?$/ + useEffect(() => { + if (!isValidBaseUrl(formData.base_url)) return; + + getServerVersion(formData.base_url) + .then(serverVersion => + setServerVersion( + `${translate("synapseadmin.auth.server_version")} ${serverVersion}` ) ) - return; - const versionUrl = `${formData.base_url}/_synapse/admin/v1/server_version`; - fetchUtils - .fetchJson(versionUrl, { method: "GET" }) - .then(({ json }) => { - setServerVersion( - `${translate("synapseadmin.auth.server_version")} ${ - json["server_version"] - }` - ); - }) - .catch(_ => { - setServerVersion(""); - }); + .catch(() => setServerVersion("")); - // Set SSO Url - const authMethodUrl = `${formData.base_url}/_matrix/client/r0/login`; - let supportPass = false, - supportSSO = false; - fetchUtils - .fetchJson(authMethodUrl, { method: "GET" }) - .then(({ json }) => { - json.flows.forEach(f => { - if (f.type === "m.login.password") { - supportPass = true; - } else if (f.type === "m.login.sso") { - supportSSO = true; - } - }); - setSupportPassAuth(supportPass); - if (supportSSO) { - setSSOBaseUrl(formData.base_url); - } else { - setSSOBaseUrl(""); - } - }) - .catch(_ => { - setSSOBaseUrl(""); - }); - }, - [formData.base_url] - ); + // Set SSO Url + getSupportedLoginFlows(formData.base_url) + .then(loginFlows => { + const supportPass = + loginFlows.find(f => f.type === "m.login.password") !== undefined; + const supportSSO = + loginFlows.find(f => f.type === "m.login.sso") !== undefined; + setSupportPassAuth(supportPass); + setSSOBaseUrl(supportSSO ? formData.base_url : ""); + }) + .catch(() => setSSOBaseUrl("")); + }, [formData.base_url]); return ( <> diff --git a/src/synapse/synapse.js b/src/synapse/synapse.js new file mode 100644 index 0000000..64003ba --- /dev/null +++ b/src/synapse/synapse.js @@ -0,0 +1,48 @@ +import { fetchUtils } from "react-admin"; + +export const splitMxid = mxid => { + const re = + /^@(?[a-zA-Z0-9._=\-/]+):(?[a-zA-Z0-9\-.]+\.[a-zA-Z]+)$/; + return re.exec(mxid)?.groups; +}; + +export const isValidBaseUrl = baseUrl => + /^(http|https):\/\/[a-zA-Z0-9\-.]+(:\d{1,5})?$/.test(baseUrl); + +/** + * Resolve the homeserver URL using the well-known lookup + * @param domain the domain part of an MXID + * @returns homeserver base URL + */ +export const getWellKnownUrl = async domain => { + const wellKnownUrl = `https://${domain}/.well-known/matrix/client`; + try { + const json = await fetchUtils.fetchJson(wellKnownUrl, { method: "GET" }); + return json["m.homeserver"].base_url; + } catch { + // if there is no .well-known entry, return the domain itself + return `https://${domain}`; + } +}; + +/** + * Get synapse server version + * @param base_url the base URL of the homeserver + * @returns server version + */ +export const getServerVersion = async baseUrl => { + const versionUrl = `${baseUrl}/_synapse/admin/v1/server_version`; + const response = await fetchUtils.fetchJson(versionUrl, { method: "GET" }); + return response.json.server_version; +}; + +/** + * Get supported login flows + * @param baseUrl the base URL of the homeserver + * @returns array of supported login flows + */ +export const getSupportedLoginFlows = async baseUrl => { + const loginFlowsUrl = `${baseUrl}/_matrix/client/r0/login`; + const response = await fetchUtils.fetchJson(loginFlowsUrl, { method: "GET" }); + return response.json.flows; +}; diff --git a/src/synapse/synapse.test.js b/src/synapse/synapse.test.js new file mode 100644 index 0000000..da777fa --- /dev/null +++ b/src/synapse/synapse.test.js @@ -0,0 +1,31 @@ +import { isValidBaseUrl, splitMxid } from "./synapse"; + +describe("splitMxid", () => { + it("splits valid MXIDs", () => + expect(splitMxid("@name:domain.tld")).toEqual({ + name: "name", + domain: "domain.tld", + })); + it("rejects invalid MXIDs", () => expect(splitMxid("foo")).toBeUndefined()); +}); + +describe("isValidBaseUrl", () => { + it("accepts a http URL", () => + expect(isValidBaseUrl("http://foo.bar")).toBeTruthy()); + it("accepts a https URL", () => + expect(isValidBaseUrl("https://foo.bar")).toBeTruthy()); + it("accepts a valid URL with port", () => + expect(isValidBaseUrl("https://foo.bar:1234")).toBeTruthy()); + it("rejects undefined base URLs", () => + expect(isValidBaseUrl(undefined)).toBeFalsy()); + it("rejects null base URLs", () => expect(isValidBaseUrl(null)).toBeFalsy()); + it("rejects empty base URLs", () => expect(isValidBaseUrl("")).toBeFalsy()); + it("rejects non-string base URLs", () => + expect(isValidBaseUrl({})).toBeFalsy()); + it("rejects base URLs without protocol", () => + expect(isValidBaseUrl("foo.bar")).toBeFalsy()); + it("rejects base URLs with path", () => + expect(isValidBaseUrl("http://foo.bar/path")).toBeFalsy()); + it("rejects invalid base URLs", () => + expect(isValidBaseUrl("http:/foo.bar")).toBeFalsy()); +});