diff --git a/src/js/login.js b/src/js/login.js index 4ae9eae..df8cf35 100644 --- a/src/js/login.js +++ b/src/js/login.js @@ -1,36 +1,67 @@ const {q, ElemJS, ejs} = require("./basic.js") +const {S_RE_DOMAIN, S_RE_IPV6, S_RE_IPV4} = require("./re.js") const password = q("#password") -const homeserver = q("#homeserver") + +// A regex matching a lossy MXID +// Groups: +// 1: username/localpart +// MAYBE WITH +// 2: hostname/serverpart +// 3: domain +// OR +// 4: IP address +// 5: IPv4 address +// OR +// 6: IPv6 address +// MAYBE WITH +// 7: port number +const RE_LOSSY_MXID = new RegExp(`^@?([a-z0-9._=-]+?)(?::((?:(${S_RE_DOMAIN})|((${S_RE_IPV4})|(${S_RE_IPV6})))(?::(\\d+))?))?$`) + +window.RE_LOSSY_MXID = RE_LOSSY_MXID class Username extends ElemJS { - constructor() { + constructor(homeserver) { super(q("#username")) + this.homeserver = homeserver; + this.on("change", this.updateServer.bind(this)) } isValid() { - return !!this.element.value.match(/^@?[a-z0-9._=\/-]+(?::[a-zA-Z0-9.:\[\]-]+)?$/) + return !!this.element.value.match(RE_LOSSY_MXID) } getUsername() { - return this.element.value.match(/^@?([a-z0-9._=\/-]+)/)[1] + return this.element.value.match(RE_LOSSY_MXID)[1] } getServer() { - const server = this.element.value.match(/^@?[a-z0-9._=\?-]+:([a-zA-Z0-9.:\[\]-]+)$/) - if (server && server[1]) return server[1] + const server = this.element.value.match(RE_LOSSY_MXID) + if (server && server[2]) return server[2] else return null } updateServer() { if (!this.isValid()) return - if (this.getServer()) homeserver.value = this.getServer() + if (this.getServer()) this.homeserver.suggest(this.getServer()) } } -const username = new Username() +class Homeserver extends ElemJS { + constructor() { + super(q("#homeserver")); + } + + suggest(value) { + this.element.placeholder = value + } + + getServer() { + return this.element.value || this.element.placeholder; + } +} class Feedback extends ElemJS { constructor() { @@ -60,13 +91,14 @@ class Feedback extends ElemJS { } } -const feedback = new Feedback() - class Form extends ElemJS { - constructor() { + constructor(username, feedback, homeserver) { super(q("#form")) this.processing = false + this.username = username + this.feedback = feedback + this.homeserver = homeserver this.on("submit", this.submit.bind(this)) } @@ -74,12 +106,12 @@ class Form extends ElemJS { async submit() { if (this.processing) return this.processing = true - if (!username.isValid()) return this.cancel("Username is not valid.") + if (!this.username.isValid()) return this.cancel("Username is not valid.") // Resolve homeserver address let domain try { - domain = await this.findHomeserver(homeserver.value) + domain = await this.findHomeserver(this.homeserver.getServer()) } catch(e) { return this.cancel(e.message) } @@ -90,7 +122,7 @@ class Form extends ElemJS { method: "POST", body: JSON.stringify({ type: "m.login.password", - user: username.getUsername(), + user: this.username.getUsername(), password: password.value }) }).then(res => res.json()) @@ -153,15 +185,21 @@ class Form extends ElemJS { } status(message) { - feedback.setLoading(true) - feedback.message(message) + this.feedback.setLoading(true) + this.feedback.message(message) } cancel(message) { this.processing = false - feedback.setLoading(false) - feedback.message(message, true) + this.feedback.setLoading(false) + this.feedback.message(message, true) } } -const form = new Form() +const homeserver = new Homeserver() + +const username = new Username(homeserver) + +const feedback = new Feedback() + +const form = new Form(username, feedback, homeserver) diff --git a/src/js/re.js b/src/js/re.js new file mode 100644 index 0000000..57b3daa --- /dev/null +++ b/src/js/re.js @@ -0,0 +1,38 @@ +// A valid internet domain, according to https://stackoverflow.com/a/20046959 (cleaned) +const S_RE_DOMAIN = "(?:[a-zA-Z]|[a-zA-Z][a-zA-Z]|[a-zA-Z]\\d|\\d[a-zA-Z]|[a-zA-Z0-9][a-zA-Z0-9-_]{1,61}[a-zA-Z0-9])\\.(?:[a-zA-Z]{2,6}|[a-zA-Z0-9-]{2,30}\\.[a-zA-Z]{2,3})" + +// A valid ipv4 address, one that doesn't check for valid numbers (e.g. not 999) and one that does +// const S_RE_IPV4_NO_CHECK = "(?:(?:\\d{1,3}\\.){3}\\d{1,3})" +const S_RE_IPV4_HAS_CHECK = "(?:(?:25[0-5]|(?:2[0-4]|1{0,1}\\d){0,1}\\d)\\.){3}(?:25[0-5]|(?:2[0-4]|1{0,1}\\d){0,1}\\d)" + +const S_RE_IPV6_SEG = "[a-fA-F\\d]{1,4}" +// Yes, this is an ipv6 address. +const S_RE_IPV6 = ` +(?: +(?:${S_RE_IPV6_SEG}:){7}(?:${S_RE_IPV6_SEG}|:)| +(?:${S_RE_IPV6_SEG}:){6}(?:${S_RE_IPV4_HAS_CHECK}|:${S_RE_IPV6_SEG}|:)| +(?:${S_RE_IPV6_SEG}:){5}(?::${S_RE_IPV4_HAS_CHECK}|(?::${S_RE_IPV6_SEG}){1,2}|:)| +(?:${S_RE_IPV6_SEG}:){4}(?:(?::${S_RE_IPV6_SEG}){0,1}:${S_RE_IPV4_HAS_CHECK}|(?::${S_RE_IPV6_SEG}){1,3}|:)| +(?:${S_RE_IPV6_SEG}:){3}(?:(?::${S_RE_IPV6_SEG}){0,2}:${S_RE_IPV4_HAS_CHECK}|(?::${S_RE_IPV6_SEG}){1,4}|:)| +(?:${S_RE_IPV6_SEG}:){2}(?:(?::${S_RE_IPV6_SEG}){0,3}:${S_RE_IPV4_HAS_CHECK}|(?::${S_RE_IPV6_SEG}){1,5}|:)| +(?:${S_RE_IPV6_SEG}:){1}(?:(?::${S_RE_IPV6_SEG}){0,4}:${S_RE_IPV4_HAS_CHECK}|(?::${S_RE_IPV6_SEG}){1,6}|:)| +(?::(?:(?::${S_RE_IPV6_SEG}){0,5}:${S_RE_IPV4_HAS_CHECK}|(?::${S_RE_IPV6_SEG}){1,7}|:)) +)(?:%[0-9a-zA-Z]{1,})?` + .replace(/\s*\/\/.*$/gm, '') + .replace(/\n/g, '') + .trim(); + +const RE_DOMAIN_EXACT = new RegExp(`^${S_RE_DOMAIN}$`) +const RE_IPV4_EXACT = new RegExp(`^${S_RE_IPV4_HAS_CHECK}$`) +const RE_IPV6_EXACT = new RegExp(`^${S_RE_IPV6}$`) +const RE_IP_ADDR_EXACT = new RegExp(`^${S_RE_IPV6}|${S_RE_IPV4_HAS_CHECK}$`) + +module.exports = { + S_RE_DOMAIN, + S_RE_IPV6, + S_RE_IPV4: S_RE_IPV4_HAS_CHECK, + RE_DOMAIN_EXACT, + RE_IPV4_EXACT, + RE_IPV6_EXACT, + RE_IP_ADDR_EXACT +} diff --git a/src/login.pug b/src/login.pug index 470ee82..c12d132 100644 --- a/src/login.pug +++ b/src/login.pug @@ -15,7 +15,7 @@ html .data-input .form-input-container label(for="username") Username - input(type="text" name="username" autocomplete="username" placeholder="@username:server.tld" pattern="^@?[a-z0-9._=/-]+(?::[a-zA-Z0-9.:\\[\\]-]+)?$" required)#username + input(type="text" name="username" autocomplete="username" placeholder="@username:server.com" pattern="^@?[a-z0-9._=/-]+(?::[a-zA-Z0-9.:\\[\\]-]+)?$" required)#username .form-input-container label(for="password") Password @@ -23,7 +23,7 @@ html .form-input-container label(for="homeserver") Homeserver - input(type="text" name="homeserver" value="matrix.org" placeholder="matrix.org" required)#homeserver + input(type="text" name="homeserver" placeholder="matrix.org")#homeserver #feedback diff --git a/src/sass/login.sass b/src/sass/login.sass index 235fad4..4d17f25 100644 --- a/src/sass/login.sass +++ b/src/sass/login.sass @@ -78,6 +78,7 @@ button, input[type="submit"] padding: 7px &:hover + cursor: pointer background-color: c.$milder label