implement simple authentication and add redirects

This commit is contained in:
hippoz 2021-11-08 00:25:27 +02:00
parent 1626cd99e5
commit 574359bd2e
No known key found for this signature in database
GPG key ID: 7C52899193467641
12 changed files with 346 additions and 38 deletions

View file

@ -77,4 +77,4 @@ class InputController():
print("got invalid code from parser (is this a bug with the MessageParser?)")
return False
return True
return True

View file

@ -1,22 +1,67 @@
import sys
from asyncio import wait_for, TimeoutError
from base64 import b64encode
from sanic import Sanic
from sanic.response import file
from sanic.response import file, redirect
from InputController import InputController
from MessageParser import MessageParser
app = Sanic("capybara")
app = Sanic(
"capybara",
load_env="CAPYBARA_"
)
input_controller = InputController()
control_message_parser = MessageParser()
# auth packet
control_message_parser.add_handler("0", {
"auth_string": "str"
})
app.ctx.input_controller = InputController()
app.static("app", "frontend/dist/", resource_type="dir")
@app.get("/")
async def home(req):
return redirect("/app/app.html")
@app.get("/app")
async def app_route(req):
return redirect("/app/app.html")
@app.websocket("/gateway")
async def gateway(req, ws):
# Before the client is able to send any data, we must await an authorization packet
try:
auth_payload = await wait_for(ws.recv(), timeout=5)
except TimeoutError:
await ws.close(code=4001, reason="Invalid auth packet")
return
code, args = control_message_parser.parse(auth_payload)
if (not code or
code != "0" or
args["auth_string"] != app.ctx.EXPECTED_AUTH_STRING): # 0 is the code for the initial auth packet
await ws.close(code=4001, reason="Invalid auth packet")
return
await ws.send("1") # send a single `1` to let the client know the server is input packets
while True:
app.ctx.input_controller.process_message(await ws.recv())
input_controller.process_message(await ws.recv())
def main():
if not app.config.get("AUTH_PASSWORD"):
print(
"capybara: FATAL ERROR: Capybara is expecting the `CAPYBARA_AUTH_PASSWORD` environment variable to be set (to a password of your choice).\nYour users will use this password to authenticate to your server.\nEXITING...",
file=sys.stderr
)
exit(1)
return
app.ctx.EXPECTED_AUTH_STRING = "%auth%" + str(b64encode(app.config.AUTH_PASSWORD.encode("utf-8")), "utf-8")
app.run(host='0.0.0.0', port=4003, access_log=False)
if __name__ == "__main__":

70
frontend/src/App.js Normal file
View file

@ -0,0 +1,70 @@
import Connection from "./Connection";
import KeyboardController from "./Keyboard";
import { getAuth, setAuth } from "./LocalConfiguration";
import LoginPrompt from "./LoginPrompt";
import TouchpadController from "./Touchpad";
class App {
constructor(mountElement) {
this.mountElement = mountElement;
this.connection = new Connection(`ws://${location.host}/gateway`);
this.touchpad = new TouchpadController(this.connection);
this.keyboard = new KeyboardController(this.connection);
this.loginPromptComponent = null;
this.connection.connect(getAuth());
this.connection.ws.addEventListener("close", ({code}) => {
this.unmountApp();
if (code === 4001) { // 4001 - code for bad auth
this.transitionTo("login");
}
});
this.connection.onHandshakeCompleted = () => {
this.transitionTo("app");
};
}
transitionTo(type) {
switch (type) {
case "login":
this.unmountApp();
this.mountLoginComponent();
break;
case "app":
this.unmountLoginComponent();
this.mountApp();
break;
default:
throw new Error(`transitionTo type ${type} is invalid`);
}
}
mountLoginComponent() {
if (!this.loginPromptComponent)
this.loginPromptComponent = new LoginPrompt(this.connection);
this.loginPromptComponent.mountOn(this.mountElement);
this.loginPromptComponent.onPasswordSubmitted = p => {
setAuth(p);
this.connection.connect(p);
};
}
unmountLoginComponent() {
if (this.loginPromptComponent)
this.loginPromptComponent.unmount();
}
mountApp() {
this.touchpad.mountOn(this.mountElement);
this.keyboard.mountOn(this.mountElement);
}
unmountApp() {
this.touchpad.unmount();
this.keyboard.unmount();
}
}
export default App;

34
frontend/src/Banner.js Normal file
View file

@ -0,0 +1,34 @@
class Banner {
constructor() {
this.text = null;
}
updateText(text) {
this.text = text;
}
mountOn(target) {
if (this.element)
return; // Already mounted
this.element = document.createRange().createContextualFragment(`
<div class="card small-card center-text">
<h2>Login</h2>
<p>You need to enter the login code before you can start controlling your device.</p>
<br>
<div class="full-width">
<input id="code-input" class="input full-width" placeholder="Code">
<br>
<button id="continue-button" class="button-default full-width">Continue</button>
</div>
</div>
`).children[0];
this.element.querySelector("#continue-button").addEventListener("click", () => {
if (this.onPasswordSubmitted)
this.onPasswordSubmitted(this.element.querySelector("#code-input").value);
});
target.appendChild(this.element);
}
}

View file

@ -6,16 +6,37 @@ class Connection {
this.log = Logger(["Connection"], ["log"]).log;
this.messageLog = Logger(["Connection", "Message"], ["log"]).log;
this.url = url;
this.isReady = false;
}
connect() {
formatAuthString(password) {
return `%auth%${btoa(password)}`;
}
connect(password) {
this.ws = new WebSocket(this.url);
this.ws.onerror = (e) => this.log("Error", e);
this.ws.onopen = () => {
this.log("Open");
this.log("Sending authentication packet");
this.ws.send(`0${this.formatAuthString(password)}`); // send auth packet
};
this.ws.onclose = () => {
this.ws.onmessage = ({ data }) => {
if (data === "1") {
if (this.onHandshakeCompleted)
this.onHandshakeCompleted();
this.isReady = true;
this.log("Handshake complete");
}
};
this.ws.onclose = ({ code }) => {
if (code === 4001) {// code for bad auth
this.log("Closed due to bad auth - skipping reconnect");
return;
}
this.log("Closed - attempting to reconnect in 4000ms");
this.isReady = false;
setTimeout(() => this.connect(), 4000);
}
}

View file

@ -4,14 +4,22 @@ class KeyboardController {
constructor(connection) {
this.connection = connection;
this.keyboard = null;
this.keyboardDiv = null;
}
_sendKeyPress(l) {
this.connection.sendMessage("k", [l]);
}
bindTo(element) {
this.keyboard = new SimpleKeyboard(element, {
mountOn(element) {
if (this.keyboardDiv)
return; // Already mounted;
this.keyboardDiv = document.createElement("div");
this.keyboardDiv.classList.add("keyboard");
element.appendChild(this.keyboardDiv);
this.keyboard = new SimpleKeyboard(this.keyboardDiv, {
onKeyPress: this.onKeyPress.bind(this),
mergeDisplay: true,
layoutName: "default",
@ -50,6 +58,14 @@ class KeyboardController {
});
}
unmount() {
if (!this.keyboardDiv)
return; // Not mounted - don't do anything
this.keyboardDiv.parentElement.removeChild(this.keyboardDiv);
this.keyboardDiv = null;
}
onKeyPress(button) {
if (button === "{shift}" || button === "{lock}")
return this.handleShift();

View file

@ -0,0 +1,7 @@
export function getAuth() {
return localStorage.getItem("$auth");
}
export function setAuth(newValue) {
return localStorage.setItem("$auth", newValue);
}

View file

@ -0,0 +1,40 @@
class LoginPrompt {
constructor() {
this.element = null;
}
mountOn(target) {
if (this.element)
return; // Already mounted
this.element = document.createRange().createContextualFragment(`
<div class="card small-card center-text">
<h2>Login</h2>
<p>You need to enter the login code before you can start controlling your device.</p>
<br>
<div class="full-width">
<input type="password" id="code-input" class="input full-width" placeholder="Code">
<br>
<button id="continue-button" class="button-default full-width">Continue</button>
</div>
</div>
`).children[0];
this.element.querySelector("#continue-button").addEventListener("click", () => {
if (this.onPasswordSubmitted)
this.onPasswordSubmitted(this.element.querySelector("#code-input").value);
});
target.appendChild(this.element);
}
unmount() {
if (!this.element)
return; // Already unmounted
this.element.parentElement.removeChild(this.element);
this.element = null;
}
}
export default LoginPrompt;

View file

@ -12,6 +12,7 @@ class TouchpadController {
this.isInHoldingMode = false;
this.ongoingTouches = {};
this.holdModeTimeout = null;
this.touchpadDiv = null;
this.connection = connection;
}
@ -59,10 +60,25 @@ class TouchpadController {
this.connection.sendMessage("c", [this.getButtonCode(button)]);
}
bindTo(element) {
element.addEventListener("touchmove", this.onTouchMove.bind(this));
element.addEventListener("touchend", this.onTouchEnd.bind(this));
element.addEventListener("touchstart", this.onTouchStart.bind(this));
mountOn(element) {
if (this.touchpadDiv)
return; // Already mounted
this.touchpadDiv = document.createElement("div");
this.touchpadDiv.classList.add("touchpad");
element.appendChild(this.touchpadDiv);
this.touchpadDiv.addEventListener("touchmove", this.onTouchMove.bind(this));
this.touchpadDiv.addEventListener("touchend", this.onTouchEnd.bind(this));
this.touchpadDiv.addEventListener("touchstart", this.onTouchStart.bind(this));
}
unmount() {
if (!this.touchpadDiv)
return; // Not mounted - don't do anything
this.touchpadDiv.parentElement.removeChild(this.touchpadDiv);
this.touchpadDiv = null;
}
onTouchMove(event) {

View file

@ -1,28 +1,5 @@
import { domLog } from "./common/Logger";
import Connection from "./Connection";
import KeyboardController from "./Keyboard";
import TouchpadController from "./Touchpad";
import "simple-keyboard/build/css/index.css";
import "./styles/main.css";
import App from "./App";
window.onerror = (error) => {
domLog(`|| ERROR: ${error} ||`);
};
const touchpadDiv = document.createElement("div");
touchpadDiv.classList.add("touchpad");
document.body.appendChild(touchpadDiv);
const keyboardDiv = document.createElement("div");
keyboardDiv.classList.add("keyboard");
document.body.appendChild(keyboardDiv);
const connection = new Connection(`ws://${location.host}/gateway`);
const controller = new TouchpadController(connection);
const keyboardController = new KeyboardController(connection);
connection.connect();
controller.bindTo(touchpadDiv);
keyboardController.bindTo(keyboardDiv);
new App(document.body);

View file

@ -23,6 +23,87 @@ body {
padding: 8px;
}
.card {
margin: 6px;
padding: 8px;
background: var(--accent-bg-color);
color: #000000;
border-radius: var(--card-border-radius);
}
.card.inner-card {
margin: 4px;
margin-bottom: 28px;
padding: 18px;
border: solid var(--accent-color) 1px;
}
.card.layout-card {
margin: 36px auto;
padding: 26px;
width: 80%;
box-shadow: 0 0 28px 3px rgba(0, 0, 0, 0.40);
}
.card.small-card {
margin: 36px auto;
padding: 26px;
width: 350px;
box-shadow: 0 0 28px 3px rgba(0, 0, 0, 0.40);
}
@media screen and (max-width: 768px) {
.card.small-card {
width: 80%;
}
}
.button-default {
display: block;
box-sizing: border-box;
padding: 12px;
margin: 4px;
text-decoration: none;
border: none;
background-color: var(--accent-bg-color);
border-radius: var(--button-border-radius);
font-size: 18px;
white-space: nowrap;
color: var(--button-accent-color);
cursor: pointer;
outline: none;
min-width: 50px;
text-align: center;
}
.full-width {
width: 100%;
}
.center-text {
text-align: center;
}
.button-default:hover:not(.button-selected) {
color: var(--accent-bg-color);
background-color: var(--hover-bg-color);
}
.button-selected {
color: var(--accent-bg-color);
background-color: var(--selected-bg-color);
}
.input {
display: block;
box-sizing: border-box;
border: none;
outline: none;
border-radius: var(--button-border-radius);
margin: 4px;
padding: 12px;
}
.touchpad {
height: clamp(5rem, 30rem, 55vh);
width: 100%;

View file

@ -14,6 +14,7 @@ module.exports = {
plugins: [
new HtmlWebpackPlugin({
title: "Capybara",
filename: "app.html"
}),
new MiniCssExtractPlugin({
filename: "[name].[contenthash].css"