This commit is contained in:
Devaev Maxim 2018-12-15 04:29:40 +03:00
parent 3445766a50
commit 3c33bd3719
74 changed files with 388 additions and 136 deletions

View File

@ -6,6 +6,7 @@ TESTENV_CMD ?= /bin/bash -c " \
(socat PTY,link=$(TESTENV_HID) PTY,link=/dev/ttyS11 &) \
&& cp -r /usr/share/kvmd/configs.default/nginx/* /etc/nginx \
&& cp /usr/share/kvmd/configs.default/kvmd/*.yaml /etc/kvmd \
&& cp /usr/share/kvmd/configs.default/kvmd/htpasswd /etc/kvmd \
&& cp /testenv/kvmd.yaml /etc/kvmd \
&& nginx -c /etc/nginx/nginx.conf \
&& ln -s $(TESTENV_VIDEO) /dev/kvmd-video \

View File

@ -14,6 +14,7 @@ depends=(
python-yaml
python-aiohttp
python-aiofiles
python-passlib
python-pyudev
python-raspberry-gpio
python-pyserial

1
configs/kvmd/htpasswd Normal file
View File

@ -0,0 +1 @@
admin:$apr1$INC0KeyU$YdLQ9qosXzNVlhxQPUf7A/

View File

@ -7,6 +7,9 @@ kvmd:
port: 8081
heartbeat: 3.0
auth:
htpasswd: /etc/kvmd/htpasswd
info:
meta: /etc/kvmd/meta.yaml
extras: /usr/share/kvmd/extras

View File

@ -1,5 +1,5 @@
# Don't touch this file otherwise your device may stop working.
# You can find a workable configuration in /usr/share/kvmd/configs.default/kvmd.
# You can find a working configuration in /usr/share/kvmd/configs.default/kvmd.
kvmd:
server:
@ -7,6 +7,9 @@ kvmd:
port: 8081
heartbeat: 3.0
auth:
htpasswd: /etc/kvmd/htpasswd
info:
meta: /etc/kvmd/meta.yaml
extras: /usr/share/kvmd/extras

View File

@ -1,5 +1,3 @@
load_module /usr/lib/nginx/modules/ngx_http_lua_module.so;
user http;
worker_processes 4;
@ -28,6 +26,7 @@ http {
tcp_nodelay on;
tcp_nopush on;
keepalive_timeout 10;
client_max_body_size 4k;
client_body_temp_path /tmp/nginx.client_body_temp;
fastcgi_temp_path /tmp/nginx.fastcgi_temp;
@ -45,11 +44,6 @@ http {
include /usr/share/kvmd/extras/*/nginx.http-ctx.conf;
#PROD lua_shared_dict WS_TOKENS 10m;
#PROD init_by_lua_block {
#PROD WS_TOKEN_EXPIRES = 10;
#PROD }
#PROD server {
#PROD listen 80;
#PROD server_name localhost;
@ -67,34 +61,47 @@ http {
#PROD add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
#PROD auth_basic "Restricted Area";
#PROD auth_basic_user_file /etc/nginx/htpasswd;
auth_request /auth;
location = /auth {
internal;
proxy_pass http://kvmd/auth/check;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
auth_request off;
}
location / {
root /usr/share/kvmd/web;
error_page 401 = @login;
error_page 403 = @login;
}
location /ws_auth {
# Workaround for Safari: https://bugs.webkit.org/show_bug.cgi?id=80362
#PROD access_by_lua_block {
#PROD local token = ngx.encode_base64(ngx.sha1_bin(ngx.var.http_Authorization));
#PROD ngx.shared.WS_TOKENS:set(token, token, WS_TOKEN_EXPIRES);
#PROD ngx.header["Set-Cookie"] = "WS_ACCESS_TOKEN=" .. token .. "; Path=/; Expires=" .. ngx.cookie_time(ngx.time() + WS_TOKEN_EXPIRES);
#PROD }
content_by_lua_block {
ngx.say("ok");
location @login {
return 302 /login;
}
location /login {
root /usr/share/kvmd/web;
auth_request off;
}
location /share {
root /usr/share/kvmd/web;
auth_request off;
}
location = /favicon.ico {
alias /usr/share/kvmd/web/favicon.ico;
auth_request off;
}
location = /robots.txt {
alias /usr/share/kvmd/web/robots.txt;
auth_request off;
}
location /kvmd/ws {
#PROD auth_basic off;
#PROD access_by_lua_block {
#PROD local token = ngx.var.cookie_WS_ACCESS_TOKEN;
#PROD local value, _ = ngx.shared.WS_TOKENS:get(token);
#PROD if value == nil then
#PROD ngx.exec("/ws_auth");
#PROD end
#PROD }
rewrite ^/kvmd/ws$ /ws break;
rewrite ^/kvmd/ws\?(.*)$ /ws?$1 break;
proxy_pass http://kvmd;
@ -104,6 +111,7 @@ http {
proxy_connect_timeout 7d;
proxy_send_timeout 7d;
proxy_read_timeout 7d;
auth_request off;
}
location /kvmd/msd/write {
@ -115,6 +123,7 @@ http {
limit_rate_after 50k;
client_max_body_size 0;
proxy_request_buffering off;
auth_request off;
}
location /kvmd/log {
@ -126,6 +135,7 @@ http {
postpone_output 0;
proxy_buffering off;
proxy_ignore_headers X-Accel-Buffering;
auth_request off;
}
location /kvmd {
@ -133,6 +143,7 @@ http {
rewrite ^/kvmd/(.*)$ /$1 break;
proxy_pass http://kvmd;
include /etc/nginx/proxy-params.conf;
auth_request off;
}
location /streamer {

View File

@ -1,6 +1,6 @@
name: KVM
description: Open KVM session in a web browser
icon: svg/kvm.svg
icon: share/svg/kvm.svg
path: kvm
keyboard_cap: true
daemon: kvmd

View File

@ -5,8 +5,9 @@ from ...logging import get_logger
from ... import gpio
from .logreader import LogReader
from .auth import AuthManager
from .info import InfoManager
from .logreader import LogReader
from .hid import Hid
from .atx import Atx
from .msd import MassStorageDevice
@ -20,6 +21,10 @@ def main() -> None:
with gpio.bcm():
loop = asyncio.get_event_loop()
auth_manager = AuthManager(
htpasswd_path=str(config["auth"]["htpasswd"]),
)
info_manager = InfoManager(
meta_path=str(config["info"]["meta"]),
extras_path=str(config["info"]["extras"]),
@ -80,6 +85,7 @@ def main() -> None:
)
Server(
auth_manager=auth_manager,
info_manager=info_manager,
log_reader=log_reader,

37
kvmd/apps/kvmd/auth.py Normal file
View File

@ -0,0 +1,37 @@
import secrets
from typing import Dict
from typing import Optional
import passlib.apache
from ...logging import get_logger
# =====
class AuthManager:
def __init__(self, htpasswd_path: str) -> None:
self.__htpasswd_path = htpasswd_path
self.__tokens: Dict[str, str] = {} # {token: user}
def login(self, user: str, passwd: str) -> Optional[str]:
htpasswd = passlib.apache.HtpasswdFile(self.__htpasswd_path)
if htpasswd.check_password(user, passwd):
for (token, token_user) in self.__tokens.items():
if user == token_user:
return token
token = secrets.token_hex(32)
self.__tokens[token] = user
get_logger().info("Logged in user %r", user)
return token
else:
get_logger().error("Access denied for user %r", user)
return None
def logout(self, token: str) -> None:
user = self.__tokens.pop(token, "")
if user:
get_logger().info("Logged out user %r", user)
def check(self, token: str) -> bool:
return (token in self.__tokens)

View File

@ -1,4 +1,5 @@
import os
import re
import signal
import socket
import asyncio
@ -23,6 +24,7 @@ from ...aioregion import RegionIsBusyError
from ... import __version__
from .auth import AuthManager
from .info import InfoManager
from .logreader import LogReader
from .hid import Hid
@ -33,8 +35,29 @@ from .streamer import Streamer
# =====
def _json(result: Optional[Dict]=None, status: int=200) -> aiohttp.web.Response:
return aiohttp.web.Response(
class HttpError(Exception):
pass
class BadRequestError(HttpError):
pass
class UnauthorizedError(HttpError):
pass
class ForbiddenError(HttpError):
pass
def _json(
result: Optional[Dict]=None,
status: int=200,
set_cookies: Optional[Dict[str, str]]=None,
) -> aiohttp.web.Response:
response = aiohttp.web.Response(
text=json.dumps({
"ok": (status == 200),
"result": (result or {}),
@ -42,11 +65,16 @@ def _json(result: Optional[Dict]=None, status: int=200) -> aiohttp.web.Response:
status=status,
content_type="application/json",
)
if set_cookies:
for (key, value) in set_cookies.items():
response.set_cookie(key, value)
return response
def _json_exception(err: Exception, status: int) -> aiohttp.web.Response:
name = type(err).__name__
msg = str(err)
if not isinstance(err, (UnauthorizedError, ForbiddenError)):
get_logger().error("API error: %s: %s", name, msg)
return _json({
"error": name,
@ -54,25 +82,36 @@ def _json_exception(err: Exception, status: int) -> aiohttp.web.Response:
}, status=status)
class BadRequestError(Exception):
pass
_ATTR_EXPOSED = "exposed"
_ATTR_EXPOSED_METHOD = "exposed_method"
_ATTR_EXPOSED_PATH = "exposed_path"
_ATTR_SYSTEM_TASK = "system_task"
_COOKIE_AUTH_TOKEN = "auth_token"
def _exposed(http_method: str, path: str) -> Callable:
def _exposed(http_method: str, path: str, auth_required: bool=True) -> Callable:
def make_wrapper(method: Callable) -> Callable:
async def wrap(self: "Server", request: aiohttp.web.Request) -> aiohttp.web.Response:
try:
if auth_required:
token = request.cookies.get(_COOKIE_AUTH_TOKEN, "")
if token:
if not self._auth_manager.check(_valid_token(token)):
raise ForbiddenError("Forbidden")
else:
raise UnauthorizedError("Unauthorized")
return (await method(self, request))
except RegionIsBusyError as err:
return _json_exception(err, 409)
except (BadRequestError, MsdOperationError) as err:
return _json_exception(err, 400)
except UnauthorizedError as err:
return _json_exception(err, 401)
except ForbiddenError as err:
return _json_exception(err, 403)
setattr(wrap, _ATTR_EXPOSED, True)
setattr(wrap, _ATTR_EXPOSED_METHOD, http_method)
@ -95,6 +134,29 @@ def _system_task(method: Callable) -> Callable:
return wrap
def _valid_user(user: Optional[str]) -> str:
if isinstance(user, str):
stripped = user.strip()
if re.match(r"^[a-z_][a-z0-9_-]*$", stripped):
return stripped
raise BadRequestError("Invalid user characters %r" % (user))
def _valid_passwd(passwd: Optional[str]) -> str:
if isinstance(passwd, str):
if re.match(r"[\x20-\x7e]*$", passwd):
return passwd
raise BadRequestError("Invalid password characters")
def _valid_token(token: Optional[str]) -> str:
if isinstance(token, str):
token = token.strip().lower()
if re.match(r"^[0-9a-f]{64}$", token):
return token
raise BadRequestError("Invalid auth token characters")
def _valid_bool(name: str, flag: Optional[str]) -> bool:
flag = str(flag).strip().lower()
if flag in ["1", "true", "yes"]:
@ -127,6 +189,7 @@ class _Events(Enum):
class Server: # pylint: disable=too-many-instance-attributes
def __init__( # pylint: disable=too-many-arguments
self,
auth_manager: AuthManager,
info_manager: InfoManager,
log_reader: LogReader,
@ -142,6 +205,7 @@ class Server: # pylint: disable=too-many-instance-attributes
loop: asyncio.AbstractEventLoop,
) -> None:
self._auth_manager = auth_manager
self.__info_manager = info_manager
self.__log_reader = log_reader
@ -210,6 +274,29 @@ class Server: # pylint: disable=too-many-instance-attributes
"extras": await self.__info_manager.get_extras(),
}
# ===== AUTH
@_exposed("POST", "/auth/login", auth_required=False)
async def __auth_login_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response:
credentials = await request.post()
token = self._auth_manager.login(
user=_valid_user(credentials.get("user", "")),
passwd=_valid_passwd(credentials.get("passwd", "")),
)
if token:
return _json({}, set_cookies={_COOKIE_AUTH_TOKEN: token})
raise ForbiddenError("Forbidden")
@_exposed("POST", "/auth/logout")
async def __auth_logout_handler(self, request: aiohttp.web.Request) -> aiohttp.web.Response:
token = _valid_token(request.cookies.get(_COOKIE_AUTH_TOKEN, ""))
self._auth_manager.logout(token)
return _json({})
@_exposed("GET", "/auth/check")
async def __auth_check_handler(self, _: aiohttp.web.Request) -> aiohttp.web.Response:
return _json({})
# ===== SYSTEM
@_exposed("GET", "/info")

View File

@ -31,12 +31,7 @@ RUN useradd -r -d / packer \
&& cd - \
&& rm -rf /tmp/packer-color
COPY testenv/customizepkg.nginx /etc/customizepkg.d/nginx-mainline-mod-ndk
COPY testenv/customizepkg.nginx /etc/customizepkg.d/nginx-mainline-mod-lua
RUN pacman -Syy \
&& user-packer -S --noconfirm \
customizepkg \
&& mkdir /.npm \
&& chmod 777 /.npm \
&& user-packer -S --noconfirm \
@ -50,7 +45,6 @@ RUN pacman -Syy \
htmlhint \
eslint \
&& rm -rf /.npm \
&& env MAKEPKGOPTS="--skipchecksums --skippgpcheck" user-packer -S --noconfirm nginx-mainline-mod-lua \
&& pacman -Sc --noconfirm
COPY testenv/requirements.txt requirements.txt

View File

@ -1 +0,0 @@
replace#global#_nginxver=.*#_nginxver=`pacman -Q nginx-mainline | grep -Po "\\d+\\.\\d+\\.\\d+"`

View File

@ -4,6 +4,9 @@ kvmd:
port: 8081
heartbeat: 3.0
auth:
htpasswd: /etc/kvmd/htpasswd
info:
meta: /etc/kvmd/meta.yaml
extras: /usr/share/kvmd/extras

View File

@ -1,6 +1,7 @@
git+git://github.com/willbuckner/rpi-gpio-development-mock@master#egg=rpi
aiohttp
aiofiles
passlib
pyudev
pyyaml
pyserial

View File

@ -33,7 +33,7 @@ deps =
[testenv:eslint]
whitelist_externals = eslint
commands = eslint --config=testenv/eslintrc.yaml --color --ext .js web/js
commands = eslint --config=testenv/eslintrc.yaml --color --ext .js web/share/js
[testenv:htmlhint]
whitelist_externals = htmlhint

View File

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#2b5797</TileColor>
</tile>
</msapplication>
</browserconfig>

View File

@ -4,22 +4,22 @@
<meta charset="utf-8" />
<title>Pi-KVM Index</title>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<link rel="apple-touch-icon" sizes="180x180" href="/share/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/share/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/share/favicon-16x16.png">
<link rel="manifest" href="/share/site.webmanifest">
<link rel="mask-icon" href="/share/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#2b5797">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="css/vars.css">
<link rel="stylesheet" href="css/main.css">
<link rel="stylesheet" href="css/modals.css">
<link rel="stylesheet" href="css/index/index.css">
<link rel="stylesheet" href="share/css/vars.css">
<link rel="stylesheet" href="share/css/main.css">
<link rel="stylesheet" href="share/css/modals.css">
<link rel="stylesheet" href="share/css/index/index.css">
<script src="js/bb.js"></script>
<script src="js/tools.js"></script>
<script src="js/index/main.js"></script>
<script src="share/js/bb.js"></script>
<script src="share/js/tools.js"></script>
<script src="share/js/index/main.js"></script>
<script>window.onload = main;</script>
</head>
@ -30,7 +30,7 @@
<table>
<tr>
<td valign="top" class="logo">
<img class="svg-gray" src="svg/logo.svg" alt="Open Source Hardware" height="40" />
<img class="svg-gray" src="share/svg/logo.svg" alt="Open Source Hardware" height="40" />
</td>
<td valign="top">
<table>

View File

@ -4,40 +4,40 @@
<meta charset="utf-8" />
<title>Pi-KVM Session</title>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5">
<link rel="apple-touch-icon" sizes="180x180" href="/share/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/share/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/share/favicon-16x16.png">
<link rel="manifest" href="/share/site.webmanifest">
<link rel="mask-icon" href="/share/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#2b5797">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="../css/vars.css">
<link rel="stylesheet" href="../css/main.css">
<link rel="stylesheet" href="../css/menu.css">
<link rel="stylesheet" href="../css/windows.css">
<link rel="stylesheet" href="../css/modals.css">
<link rel="stylesheet" href="../css/leds.css">
<link rel="stylesheet" href="../css/sliders.css">
<link rel="stylesheet" href="../css/switches.css">
<link rel="stylesheet" href="../css/progress.css">
<link rel="stylesheet" href="../css/kvm/stream.css">
<link rel="stylesheet" href="../css/kvm/hid.css">
<link rel="stylesheet" href="../css/kvm/msd.css">
<link rel="stylesheet" href="../css/kvm/keyboard.css">
<link rel="stylesheet" href="../css/kvm/about.css">
<link rel="stylesheet" href="../share/css/vars.css">
<link rel="stylesheet" href="../share/css/main.css">
<link rel="stylesheet" href="../share/css/menu.css">
<link rel="stylesheet" href="../share/css/windows.css">
<link rel="stylesheet" href="../share/css/modals.css">
<link rel="stylesheet" href="../share/css/leds.css">
<link rel="stylesheet" href="../share/css/sliders.css">
<link rel="stylesheet" href="../share/css/switches.css">
<link rel="stylesheet" href="../share/css/progress.css">
<link rel="stylesheet" href="../share/css/kvm/stream.css">
<link rel="stylesheet" href="../share/css/kvm/hid.css">
<link rel="stylesheet" href="../share/css/kvm/msd.css">
<link rel="stylesheet" href="../share/css/kvm/keyboard.css">
<link rel="stylesheet" href="../share/css/kvm/about.css">
<script src="../js/bb.js"></script>
<script src="../js/tools.js"></script>
<script src="../js/wm.js"></script>
<script src="../js/kvm/stream.js"></script>
<script src="../js/kvm/atx.js"></script>
<script src="../js/kvm/keyboard.js"></script>
<script src="../js/kvm/mouse.js"></script>
<script src="../js/kvm/hid.js"></script>
<script src="../js/kvm/msd.js"></script>
<script src="../js/kvm/session.js"></script>
<script src="../js/kvm/main.js"></script>
<script src="../share/js/bb.js"></script>
<script src="../share/js/tools.js"></script>
<script src="../share/js/wm.js"></script>
<script src="../share/js/kvm/stream.js"></script>
<script src="../share/js/kvm/atx.js"></script>
<script src="../share/js/kvm/keyboard.js"></script>
<script src="../share/js/kvm/mouse.js"></script>
<script src="../share/js/kvm/hid.js"></script>
<script src="../share/js/kvm/msd.js"></script>
<script src="../share/js/kvm/session.js"></script>
<script src="../share/js/kvm/main.js"></script>
<script>window.onload = main;</script>
</head>
@ -47,16 +47,16 @@
<li class="menu-left-items">
<a id="menu-logo" href="/">
&#8617;&nbsp;&nbsp;
<img class="svg-gray" src="../svg/logo.svg" alt="&pi;-kvm" />
<img class="svg-gray" src="../share/svg/logo.svg" alt="&pi;-kvm" />
</a>
</li>
<li class="menu-right-items">
<a class="menu-item" href="#">
<img data-dont-hide-menu id="link-led" class="led-gray" src="../svg/link-led.svg" />
<img data-dont-hide-menu id="stream-led" class="led-gray" src="../svg/stream-led.svg" />
<img data-dont-hide-menu id="hid-keyboard-led" class="led-gray" src="../svg/hid-keyboard-led.svg" />
<img data-dont-hide-menu id="hid-mouse-led" class="led-gray" src="../svg/hid-mouse-led.svg" />
<img data-dont-hide-menu id="link-led" class="led-gray" src="../share/svg/link-led.svg" />
<img data-dont-hide-menu id="stream-led" class="led-gray" src="../share/svg/stream-led.svg" />
<img data-dont-hide-menu id="hid-keyboard-led" class="led-gray" src="../share/svg/hid-keyboard-led.svg" />
<img data-dont-hide-menu id="hid-mouse-led" class="led-gray" src="../share/svg/hid-mouse-led.svg" />
System &#8628;
</a>
<div data-dont-hide-menu class="menu-item-content">
@ -118,8 +118,8 @@
<li class="menu-right-items">
<a class="menu-item" href="#">
<img data-dont-hide-menu id="atx-power-led" class="led-gray" src="../svg/atx-power-led.svg" />
<img data-dont-hide-menu id="atx-hdd-led" class="led-gray" src="../svg/atx-hdd-led.svg" />
<img data-dont-hide-menu id="atx-power-led" class="led-gray" src="../share/svg/atx-power-led.svg" />
<img data-dont-hide-menu id="atx-hdd-led" class="led-gray" src="../share/svg/atx-hdd-led.svg" />
ATX &#8628;
</a>
<div class="menu-item-content menu-item-content-buttons">
@ -132,7 +132,7 @@
<li class="menu-right-items">
<a class="menu-item" href="#">
<img data-dont-hide-menu id="msd-led" class="led-gray" src="../svg/msd-led.svg" />
<img data-dont-hide-menu id="msd-led" class="led-gray" src="../share/svg/msd-led.svg" />
Mass Storage &#8628;
</a>
<div data-dont-hide-menu id="msd-menu" class="menu-item-content">
@ -140,7 +140,7 @@
<div class="menu-item-content-text">
<table>
<tr>
<td><img src="../svg/warning.svg" /></td>
<td><img src="../share/svg/warning.svg" /></td>
<td><b>Mass Storage Device is not operational</b></td>
</tr>
</table>
@ -152,7 +152,7 @@
<div class="menu-item-content-text">
<table>
<tr>
<td><img src="../svg/warning.svg" /></td>
<td><img src="../share/svg/warning.svg" /></td>
<td><b>Current image is broken!</b><br><sub>Perhaps uploading was interrupted</sub></td>
</tr>
</table>
@ -164,7 +164,7 @@
<div class="menu-item-content-text">
<table>
<tr>
<td><img src="../svg/info.svg" /></td>
<td><img src="../share/svg/info.svg" /></td>
<td><b>Another user uploads an image</b></td>
</tr>
</table>
@ -233,7 +233,7 @@
<li class="menu-right-items">
<a class="menu-item" href="#">
<img data-dont-hide-menu id="hid-pak-led" class="led-gray" src="../svg/gear-led.svg" />
<img data-dont-hide-menu id="hid-pak-led" class="led-gray" src="../share/svg/gear-led.svg" />
Shortcuts &#8628;
</a>
<div data-dont-hide-menu class="menu-item-content">
@ -282,7 +282,7 @@
</div>
<div id="stream-info"></div>
<div id="stream-box" class="stream-box-inactive">
<img id="stream-image" class="stream-image-inactive" src="../png/blank-stream.png" />
<img id="stream-image" class="stream-image-inactive" src="../share/png/blank-stream.png" />
</div>
<div id="stream-mouse-buttons">
<button data-mouse-button="left" class="row50">Left Click</button>
@ -534,7 +534,7 @@
<table>
<tr>
<td valign="top" class="logo">
<img class="svg-gray" src="../svg/logo.svg" alt="Open Source Hardware" height="40" />
<img class="svg-gray" src="../share/svg/logo.svg" alt="Open Source Hardware" height="40" />
</td>
<td valign="top">
<table>

47
web/login/index.html Normal file
View File

@ -0,0 +1,47 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Pi-KVM Login</title>
<link rel="apple-touch-icon" sizes="180x180" href="/share/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/share/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/share/favicon-16x16.png">
<link rel="manifest" href="/share/site.webmanifest">
<link rel="mask-icon" href="/share/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#2b5797">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="../share/css/vars.css">
<link rel="stylesheet" href="../share/css/main.css">
<link rel="stylesheet" href="../share/css/modals.css">
<link rel="stylesheet" href="../share/css/login/login.css">
<script src="../share/js/bb.js"></script>
<script src="../share/js/tools.js"></script>
<script src="../share/js/login/main.js"></script>
<script>window.onload = main;</script>
</head>
<body>
<div id="login-box">
<div id="login">
<table>
<tr>
<td>Username:</td>
<td><input type="text" id="user-input"></td>
</tr>
<tr>
<td>Password:</td>
<td><input type="password" id="passwd-input"></td>
</tr>
<tr>
<td></td>
<td><button id="login-button">Login</button></td>
</tr>
</table>
</div>
</div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.6 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@ -0,0 +1,25 @@
div#login-box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
min-height: 100vh;
}
div#login {
text-align: left;
outline: none;
word-wrap: break-word;
max-width: 400px;
border: var(--border-window-thin);
border-radius: 8px;
box-sizing: border-box;
box-shadow: var(--shadow-big);
background-color: var(--cs-window-default-bg);
padding: 15px;
}
input[type="text"]#user-input, input[type="password"]#passwd-input {
text-align: center;
}

View File

@ -62,6 +62,7 @@ img.svg-gray {
button, select {
box-shadow: none;
border: none;
border-radius: 4px;
color: var(--cs-control-default-fg);
background-color: var(--cs-control-default-bg);
display: block;
@ -116,8 +117,18 @@ select:active {
background-image: url("../svg/select-arrow-intensive.svg") !important;
}
input[type=text], input[type=password] {
overflow-x: auto;
font-family: monospace;
border: thin;
border-radius: 4px;
color: var(--cs-code-default-fg);
background-color: var(--cs-code-default-bg);
padding: 2px;
}
@media only screen and (min-width: 768px) and (max-width: 1024px) and (orientation: portrait) {
button, select {
button, select, input[type=text], input[type=password] {
height: 45px !important;
}
}

View File

@ -98,6 +98,7 @@ ul#menu li div.menu-item-content-text {
}
ul#menu li div.menu-item-content button, select {
border-radius: 0;
text-align: left;
padding: 0 16px;
}

View File

@ -47,6 +47,7 @@ div.modal div.modal-window div.modal-buttons {
}
div.modal div.modal-window div.modal-buttons button {
border-radius: 0;
height: 40px;
}
@media only screen and (min-width: 768px) and (max-width: 1024px) and (orientation: portrait) {

View File

Before

Width:  |  Height:  |  Size: 638 B

After

Width:  |  Height:  |  Size: 638 B

View File

Before

Width:  |  Height:  |  Size: 937 B

After

Width:  |  Height:  |  Size: 937 B

View File

@ -14,8 +14,7 @@ function Session() {
var __streamer = new Streamer();
var __init__ = function() {
$("link-led").title = "Not connected yet...";
__startPoller();
__startSession();
};
/********************************************************************************/
@ -44,24 +43,15 @@ function Session() {
$("about-version-streamer").innerHTML = `${state.version.streamer} (${state.streamer})`;
};
var __startPoller = function() {
var __startSession = function() {
$("link-led").className = "led-yellow";
$("link-led").title = "Connecting...";
var http = tools.makeRequest("GET", "/ws_auth", function() {
if (http.readyState === 4) {
if (http.status === 200) {
var proto = (location.protocol === "https:" ? "wss" : "ws");
__ws = new WebSocket(`${proto}://${location.host}/kvmd/ws`);
__ws.onopen = __wsOpenHandler;
__ws.onmessage = __wsMessageHandler;
__ws.onerror = __wsErrorHandler;
__ws.onclose = __wsCloseHandler;
} else {
__wsCloseHandler(null);
}
}
});
};
var __wsOpenHandler = function(event) {
@ -118,7 +108,7 @@ function Session() {
setTimeout(function() {
$("link-led").className = "led-yellow";
setTimeout(__startPoller, 500);
setTimeout(__startSession, 500);
}, 500);
};

View File

@ -0,0 +1,36 @@
function main() {
if (checkBrowser()) {
tools.setOnClick($("login-button"), __login);
document.onkeyup = function(event) {
if (event.code == "Enter") {
event.preventDefault();
__login();
}
};
$("user-input").focus();
}
}
function __login() {
var user = $("user-input").value;
var passwd = $("passwd-input").value;
var body = `user=${encodeURIComponent(user)}&passwd=${encodeURIComponent(passwd)}`;
var http = tools.makeRequest("POST", "/kvmd/auth/login", function() {
if (http.readyState === 4) {
if (http.status === 200) {
document.location.href = "/";
}
__setDisabled(false);
$("passwd-input").focus();
$("passwd-input").select();
}
}, body, "application/x-www-form-urlencoded");
http.send();
__setDisabled(true);
}
function __setDisabled(disabled) {
$("user-input").disabled = disabled;
$("passwd-input").disabled = disabled;
$("login-button").disabled = disabled;
}

View File

@ -1,12 +1,15 @@
var tools = new function() {
var __debug = (new URL(window.location.href)).searchParams.get("debug");
this.makeRequest = function(method, url, callback, timeout=null) {
this.makeRequest = function(method, url, callback, body=null, content_type=null) {
var http = new XMLHttpRequest();
http.open(method, url, true);
if (content_type) {
http.setRequestHeader("Content-Type", content_type);
}
http.onreadystatechange = callback;
http.timeout = (timeout ? timeout : 5000);
http.send();
http.timeout = 5000;
http.send(body);
return http;
};

View File

Before

Width:  |  Height:  |  Size: 339 KiB

After

Width:  |  Height:  |  Size: 339 KiB

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -3,7 +3,7 @@
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"src": "/share/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
}

View File

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

Before

Width:  |  Height:  |  Size: 4.8 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

View File

Before

Width:  |  Height:  |  Size: 9.8 KiB

After

Width:  |  Height:  |  Size: 9.8 KiB

View File

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View File

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

Before

Width:  |  Height:  |  Size: 328 B

After

Width:  |  Height:  |  Size: 328 B

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB