implement simple authentication and add redirects
This commit is contained in:
parent
1626cd99e5
commit
574359bd2e
12 changed files with 346 additions and 38 deletions
53
capybara.py
53
capybara.py
|
@ -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
70
frontend/src/App.js
Normal 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
34
frontend/src/Banner.js
Normal 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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
7
frontend/src/LocalConfiguration.js
Normal file
7
frontend/src/LocalConfiguration.js
Normal file
|
@ -0,0 +1,7 @@
|
|||
export function getAuth() {
|
||||
return localStorage.getItem("$auth");
|
||||
}
|
||||
|
||||
export function setAuth(newValue) {
|
||||
return localStorage.setItem("$auth", newValue);
|
||||
}
|
40
frontend/src/LoginPrompt.js
Normal file
40
frontend/src/LoginPrompt.js
Normal 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;
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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%;
|
||||
|
|
|
@ -14,6 +14,7 @@ module.exports = {
|
|||
plugins: [
|
||||
new HtmlWebpackPlugin({
|
||||
title: "Capybara",
|
||||
filename: "app.html"
|
||||
}),
|
||||
new MiniCssExtractPlugin({
|
||||
filename: "[name].[contenthash].css"
|
||||
|
|
Loading…
Reference in a new issue