diff --git a/TODO.txt b/TODO.txt index 6075e19..0bdce5c 100644 --- a/TODO.txt +++ b/TODO.txt @@ -91,8 +91,8 @@ Milestone: v0.3.0 - Ensure that registration tokens can be used even if registration is disabled. [~] User-Interactive fallback - [ ] Password - [ ] Registration token + [~] Password + [~] Registration token [~] 4: Account management [~] Deactivate [x] Make sure UserLogin() fails if user is deactivated. diff --git a/src/Html.c b/src/Html.c index 25ea8ce..5957891 100644 --- a/src/Html.c +++ b/src/Html.c @@ -44,64 +44,13 @@ HtmlBegin(Stream * stream, char *title) "" "" "
" ,title ); - HtmlBeginStyle(stream); - StreamPuts(stream, - ":root {" - " color-scheme: dark;" - " --accent: #7b8333;" - "}" - "body {" - " margin: auto;" - " width: 100%;" - " max-width: 8.5in;" - " padding: 0.25in;" - " background-color: #0d1117;" - " color: white;" - "}" - "a {" - " color: var(--accent);" - " text-decoration: none;" - "}" - "h1 {" - " text-align: center;" - "}" - ".logo {" - " color: var(--accent);" - " text-align: center;" - " font-weight: bold;" - "}" - ); - - StreamPuts(stream, - ".form {" - " margin: auto;" - " width: 100%;" - " max-width: 400px;" - " border-radius: 10px;" - " border: 1px var(--accent) solid;" - " padding: 10px;" - "}" - "form {" - " display: block;" - "}" - "form > input, label {" - " width: 95%;" - " height: 25px;" - " display: block;" - " margin-bottom: 5px;" - " margin-left: auto;" - " margin-right: auto;" - "}" - ); - HtmlEndStyle(stream); - - StreamPuts(stream, - "" - "" - "" - ); for (i = 0; i < TELODENDRIA_LOGO_HEIGHT; i++) { diff --git a/src/Routes.c b/src/Routes.c index 3cf53e2..1f63d97 100644 --- a/src/Routes.c +++ b/src/Routes.c @@ -46,6 +46,7 @@ RouterBuild(void) R("/_matrix/client/versions", RouteVersions); R("/_matrix/static", RouteStaticDefault); + R("/_matrix/static/telodendria\\.(js|css)", RouteStaticResources); R("/_matrix/static/client/login", RouteStaticLogin); R("/_matrix/client/v3/auth/(.*)/fallback/web", RouteUiaFallback); diff --git a/src/Routes/RouteStaticLogin.c b/src/Routes/RouteStaticLogin.c index b417b11..465fdf2 100644 --- a/src/Routes/RouteStaticLogin.c +++ b/src/Routes/RouteStaticLogin.c @@ -36,56 +36,18 @@ ROUTE_IMPL(RouteStaticLogin, path, argp) HtmlBegin(stream, "Log In"); + HtmlBeginForm(stream, "login-form"); StreamPuts(stream, - "" - ""); - - HtmlBeginStyle(stream); - StreamPuts(stream, - "#error-msg {" - " display: none;" - " color: red;" - " text-align: center;" - " font-weight: bold;" - " font-size: larger;" - "}"); - HtmlEndStyle(stream); - - StreamPuts(stream, - "" - "" ); + HtmlEndForm(stream); HtmlBeginJs(stream); - StreamPuts(stream, - "function findGetParameter(parameterName) {" - " var result = null;" - " var tmp = [];" - " var items = location.search.substr(1).split(\"&\");" - " for (var index = 0; index < items.length; index++) {" - " tmp = items[index].split(\"=\");" - " if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);" - " }" - " return result;" - "}" - "function setError(msg) {" - " var err = document.getElementById('error-msg');" - " if (msg) {" - " err.style.display = 'block';" - " err.innerHTML = msg;" - " } else {" - " err.style.display = 'none';" - " }" - "}" - ); - StreamPuts(stream, "function buildRequest(user, pass) {" " var d = findGetParameter('device_id');" @@ -113,39 +75,26 @@ ROUTE_IMPL(RouteStaticLogin, path, argp) " if (window.onLogin) {" " window.onLogin(r);" " } else {" - " setError('Client malfunction. Your client should have defined window.onLogin()');" + " setFormError('Client error.');" " }" " } else {" - " setError(r.errcode + ': ' + r.error);" + " setFormError(r.errcode + ': ' + r.error);" " }" " }" "}" ); StreamPuts(stream, - "function sendRequest(request) {" - " var xhr = new XMLHttpRequest();" - " xhr.open('POST', '/_matrix/client/v3/login');" - " xhr.setRequestHeader('Content-Type', 'application/json');" - " xhr.onreadystatechange = () => processResponse(xhr);" - " xhr.send(JSON.stringify(request));" - "}" - ); - - StreamPuts(stream, - "window.addEventListener('load', () => {" - " document.getElementById('login-form').addEventListener('submit', (e) => {" - " e.preventDefault();" - " var user = document.getElementById('user').value;" - " var pass = document.getElementById('password').value;" - " if (!user || !pass) {" - " setError('Please provide a username and password.');" - " return;" - " }" - " setError(null);" - " var request = buildRequest(user, pass);" - " sendRequest(request);" - " });" + "onFormSubmit('login-form', (frm) => {" + " var user = document.getElementById('user').value;" + " var pass = document.getElementById('password').value;" + " if (!user || !pass) {" + " setFormError('Please provide a username and password.');" + " return;" + " }" + " setFormError(null);" + " var request = buildRequest(user, pass);" + " jsonRequest('POST', '/_matrix/client/v3/login', request, processResponse);" "});" ); HtmlEndJs(stream); diff --git a/src/Routes/RouteStaticResources.c b/src/Routes/RouteStaticResources.c new file mode 100644 index 0000000..b04450d --- /dev/null +++ b/src/Routes/RouteStaticResources.c @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2022-2023 Jordan Bancino <@jordan:bancino.net> + * + * Permission is hereby granted, free of charge, to any person + * obtaining a copy of this software and associated documentation files + * (the "Software"), to deal in the Software without restriction, + * including without limitation the rights to use, copy, modify, merge, + * publish, distribute, sublicense, and/or sell copies of the Software, + * and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND + * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS + * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN + * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN + * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#include+ +ROUTE_IMPL(RouteStaticResources, path, argp) +{ + RouteArgs *args = argp; + Stream *stream = HttpServerStream(args->context); + char *res = ArrayGet(path, 0); + + if (!res) + { + /* Should be impossible */ + HttpResponseStatus(args->context, HTTP_INTERNAL_SERVER_ERROR); + return MatrixErrorCreate(M_UNKNOWN); + } + + if (strcmp(res, "js") == 0) + { + HttpResponseHeader(args->context, "Content-Type", "text/javascript"); + HttpSendHeaders(args->context); + + StreamPuts(stream, + "function findGetParameter(parameterName) {" + " var result = null;" + " var tmp = [];" + " var items = location.search.substr(1).split(\"&\");" + " for (var index = 0; index < items.length; index++) {" + " tmp = items[index].split(\"=\");" + " if (tmp[0] === parameterName) result = decodeURIComponent(tmp[1]);" + " }" + " return result;" + "}" + "function setFormError(msg) {" + " var err = document.getElementById('error-msg');" + " if (msg) {" + " err.style.display = 'block';" + " err.innerHTML = msg;" + " } else {" + " err.style.display = 'none';" + " }" + "}" + ); + StreamPuts(stream, + "function jsonRequest(meth, url, json, cb) {" + " var xhr = new XMLHttpRequest();" + " xhr.open(meth, url);" + " xhr.setRequestHeader('Content-Type', 'application/json');" + " xhr.onreadystatechange = () => cb(xhr);" + " xhr.send(JSON.stringify(json));" + "}" + "function onFormSubmit(frm, cb) {" + " window.addEventListener('load', () => {" + " frm = document.getElementById(frm);" + " frm.addEventListener('submit', (e) => {" + " e.preventDefault();" + " cb(frm);" + " });" + " });" + "}" + ); + + } + else if (strcmp(res, "css") == 0) + { + HttpResponseHeader(args->context, "Content-Type", "text/css"); + HttpSendHeaders(args->context); + StreamPuts(stream, + ":root {" + " color-scheme: dark;" + " --accent: #7b8333;" + "}" + "body {" + " margin: auto;" + " width: 100%;" + " max-width: 8.5in;" + " padding: 0.25in;" + " background-color: #0d1117;" + " color: white;" + "}" + "a {" + " color: var(--accent);" + " text-decoration: none;" + "}" + "h1 {" + " text-align: center;" + "}" + ".logo {" + " color: var(--accent);" + " text-align: center;" + " font-weight: bold;" + "}"); + StreamPuts(stream, + ".form {" + " margin: auto;" + " width: 100%;" + " max-width: 400px;" + " border-radius: 10px;" + " border: 1px var(--accent) solid;" + " padding: 10px;" + "}" + "form {" + " display: block;" + "}" + "form > input, label {" + " width: 95%;" + " height: 25px;" + " display: block;" + " margin-bottom: 5px;" + " margin-left: auto;" + " margin-right: auto;" + "}" + ".form > #error-msg {" + " display: none;" + " color: red;" + " text-align: center;" + " font-weight: bold;" + " font-size: larger;" + "}"); + } + else + { + HttpResponseStatus(args->context, HTTP_NOT_FOUND); + return MatrixErrorCreate(M_NOT_FOUND); + } + + + return NULL; +} diff --git a/src/Routes/RouteUiaFallback.c b/src/Routes/RouteUiaFallback.c index dfd7114..d3d150b 100644 --- a/src/Routes/RouteUiaFallback.c +++ b/src/Routes/RouteUiaFallback.c @@ -50,17 +50,43 @@ ROUTE_IMPL(RouteUiaFallback, path, argp) HttpSendHeaders(args->context); HtmlBegin(stream, "Authentication"); - if (strcmp(authType, "m.login.dummy") == 0) - { - /* TODO */ - } - else if (strcmp(authType, "m.login.password") == 0) + if (strcmp(authType, "m.login.password") == 0) { + HtmlBeginForm(stream, "auth-form"); + StreamPuts(stream, + "" + "" + "" + "" + "
" + ""); + HtmlEndForm(stream); + HtmlBeginJs(stream); /* TODO */ + StreamPuts(stream, + "function buildRequest() {" + " setFormError('Not implemented yet.');" + " return false;" + "}"); + HtmlEndJs(stream); } else if (strcmp(authType, "m.login.registration_token") == 0) { + HtmlBeginForm(stream, "auth-form"); + StreamPuts(stream, + "" + "" + "
" + ""); + HtmlEndForm(stream); + HtmlBeginJs(stream); /* TODO */ + StreamPuts(stream, + "function buildRequest() {" + " setFormError('Not implemented yet.');" + " return false;" + "}"); + HtmlEndJs(stream); } /* * TODO: implement m.login.recaptcha, m.login.sso, @@ -71,9 +97,36 @@ ROUTE_IMPL(RouteUiaFallback, path, argp) HttpResponseStatus(args->context, HTTP_NOT_FOUND); StreamPrintf(stream, "Unknown auth type:
", authType); + goto finish; } - HtmlEnd(stream); + HtmlBeginJs(stream); + StreamPuts(stream, + "function processResponse(xhr) {" + " if (xhr.status == 200) {" + " if (window.onAuthDone) {" + " window.onAuthDone();" + " } else if (window.opener && window.opener.postMessage) {" + " window.opener.postMessage('authDone', '*');" + " } else {" + " setFormError('Client error.');" + " }" + " } else {" + " let r = JSON.parse(xhr.responseText);" + " setFormError(`${r.errcode}: ${r.error}`);" + " }" + "}"); + StreamPuts(stream, + "onFormSubmit('auth-form', (frm) => {" + " let request = buildRequest();" + " if (request) {" + " jsonRequest('POST', window.location.pathname, request, processResponse);" + " }" + "});"); + HtmlEndJs(stream); + +finish: + HtmlEnd(stream); return NULL; } diff --git a/src/include/Html.h b/src/include/Html.h index 8919295..172dc71 100644 --- a/src/include/Html.h +++ b/src/include/Html.h @@ -37,6 +37,15 @@ #define HtmlBeginStyle(stream) StreamPuts(stream, "") +#define HtmlBeginForm(stream, id) StreamPrintf(stream, \ + "%s
" \ + "" \ + "" \ + ""); + extern void HtmlBegin(Stream *, char *); diff --git a/src/include/Routes.h b/src/include/Routes.h index 385026c..d177b61 100644 --- a/src/include/Routes.h +++ b/src/include/Routes.h @@ -68,6 +68,7 @@ ROUTE(RouteRequestToken); ROUTE(RouteUiaFallback); ROUTE(RouteStaticDefault); ROUTE(RouteStaticLogin); +ROUTE(RouteStaticResources); ROUTE(RouteProcControl); ROUTE(RouteConfig);