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 &) \ (socat PTY,link=$(TESTENV_HID) PTY,link=/dev/ttyS11 &) \
&& cp -r /usr/share/kvmd/configs.default/nginx/* /etc/nginx \ && 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/*.yaml /etc/kvmd \
&& cp /usr/share/kvmd/configs.default/kvmd/htpasswd /etc/kvmd \
&& cp /testenv/kvmd.yaml /etc/kvmd \ && cp /testenv/kvmd.yaml /etc/kvmd \
&& nginx -c /etc/nginx/nginx.conf \ && nginx -c /etc/nginx/nginx.conf \
&& ln -s $(TESTENV_VIDEO) /dev/kvmd-video \ && ln -s $(TESTENV_VIDEO) /dev/kvmd-video \

View File

@ -14,6 +14,7 @@ depends=(
python-yaml python-yaml
python-aiohttp python-aiohttp
python-aiofiles python-aiofiles
python-passlib
python-pyudev python-pyudev
python-raspberry-gpio python-raspberry-gpio
python-pyserial 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 port: 8081
heartbeat: 3.0 heartbeat: 3.0
auth:
htpasswd: /etc/kvmd/htpasswd
info: info:
meta: /etc/kvmd/meta.yaml meta: /etc/kvmd/meta.yaml
extras: /usr/share/kvmd/extras extras: /usr/share/kvmd/extras

View File

@ -1,5 +1,5 @@
# Don't touch this file otherwise your device may stop working. # 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: kvmd:
server: server:
@ -7,6 +7,9 @@ kvmd:
port: 8081 port: 8081
heartbeat: 3.0 heartbeat: 3.0
auth:
htpasswd: /etc/kvmd/htpasswd
info: info:
meta: /etc/kvmd/meta.yaml meta: /etc/kvmd/meta.yaml
extras: /usr/share/kvmd/extras extras: /usr/share/kvmd/extras

View File

@ -1,5 +1,3 @@
load_module /usr/lib/nginx/modules/ngx_http_lua_module.so;
user http; user http;
worker_processes 4; worker_processes 4;
@ -28,6 +26,7 @@ http {
tcp_nodelay on; tcp_nodelay on;
tcp_nopush on; tcp_nopush on;
keepalive_timeout 10; keepalive_timeout 10;
client_max_body_size 4k;
client_body_temp_path /tmp/nginx.client_body_temp; client_body_temp_path /tmp/nginx.client_body_temp;
fastcgi_temp_path /tmp/nginx.fastcgi_temp; fastcgi_temp_path /tmp/nginx.fastcgi_temp;
@ -45,11 +44,6 @@ http {
include /usr/share/kvmd/extras/*/nginx.http-ctx.conf; 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 server {
#PROD listen 80; #PROD listen 80;
#PROD server_name localhost; #PROD server_name localhost;
@ -67,34 +61,47 @@ http {
#PROD add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always; #PROD add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
#PROD auth_basic "Restricted Area"; auth_request /auth;
#PROD auth_basic_user_file /etc/nginx/htpasswd;
location = /auth {
internal;
proxy_pass http://kvmd/auth/check;
proxy_pass_request_body off;
proxy_set_header Content-Length "";
auth_request off;
}
location / { location / {
root /usr/share/kvmd/web; root /usr/share/kvmd/web;
error_page 401 = @login;
error_page 403 = @login;
} }
location /ws_auth { location @login {
# Workaround for Safari: https://bugs.webkit.org/show_bug.cgi?id=80362 return 302 /login;
#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 {
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 { 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 break;
rewrite ^/kvmd/ws\?(.*)$ /ws?$1 break; rewrite ^/kvmd/ws\?(.*)$ /ws?$1 break;
proxy_pass http://kvmd; proxy_pass http://kvmd;
@ -104,6 +111,7 @@ http {
proxy_connect_timeout 7d; proxy_connect_timeout 7d;
proxy_send_timeout 7d; proxy_send_timeout 7d;
proxy_read_timeout 7d; proxy_read_timeout 7d;
auth_request off;
} }
location /kvmd/msd/write { location /kvmd/msd/write {
@ -115,6 +123,7 @@ http {
limit_rate_after 50k; limit_rate_after 50k;
client_max_body_size 0; client_max_body_size 0;
proxy_request_buffering off; proxy_request_buffering off;
auth_request off;
} }
location /kvmd/log { location /kvmd/log {
@ -126,6 +135,7 @@ http {
postpone_output 0; postpone_output 0;
proxy_buffering off; proxy_buffering off;
proxy_ignore_headers X-Accel-Buffering; proxy_ignore_headers X-Accel-Buffering;
auth_request off;
} }
location /kvmd { location /kvmd {
@ -133,6 +143,7 @@ http {
rewrite ^/kvmd/(.*)$ /$1 break; rewrite ^/kvmd/(.*)$ /$1 break;
proxy_pass http://kvmd; proxy_pass http://kvmd;
include /etc/nginx/proxy-params.conf; include /etc/nginx/proxy-params.conf;
auth_request off;
} }
location /streamer { location /streamer {

View File

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

View File

@ -5,8 +5,9 @@ from ...logging import get_logger
from ... import gpio from ... import gpio
from .logreader import LogReader from .auth import AuthManager
from .info import InfoManager from .info import InfoManager
from .logreader import LogReader
from .hid import Hid from .hid import Hid
from .atx import Atx from .atx import Atx
from .msd import MassStorageDevice from .msd import MassStorageDevice
@ -20,6 +21,10 @@ def main() -> None:
with gpio.bcm(): with gpio.bcm():
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
auth_manager = AuthManager(
htpasswd_path=str(config["auth"]["htpasswd"]),
)
info_manager = InfoManager( info_manager = InfoManager(
meta_path=str(config["info"]["meta"]), meta_path=str(config["info"]["meta"]),
extras_path=str(config["info"]["extras"]), extras_path=str(config["info"]["extras"]),
@ -80,6 +85,7 @@ def main() -> None:
) )
Server( Server(
auth_manager=auth_manager,
info_manager=info_manager, info_manager=info_manager,
log_reader=log_reader, 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 os
import re
import signal import signal
import socket import socket
import asyncio import asyncio
@ -23,6 +24,7 @@ from ...aioregion import RegionIsBusyError
from ... import __version__ from ... import __version__
from .auth import AuthManager
from .info import InfoManager from .info import InfoManager
from .logreader import LogReader from .logreader import LogReader
from .hid import Hid from .hid import Hid
@ -33,8 +35,29 @@ from .streamer import Streamer
# ===== # =====
def _json(result: Optional[Dict]=None, status: int=200) -> aiohttp.web.Response: class HttpError(Exception):
return aiohttp.web.Response( 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({ text=json.dumps({
"ok": (status == 200), "ok": (status == 200),
"result": (result or {}), "result": (result or {}),
@ -42,11 +65,16 @@ def _json(result: Optional[Dict]=None, status: int=200) -> aiohttp.web.Response:
status=status, status=status,
content_type="application/json", 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: def _json_exception(err: Exception, status: int) -> aiohttp.web.Response:
name = type(err).__name__ name = type(err).__name__
msg = str(err) msg = str(err)
if not isinstance(err, (UnauthorizedError, ForbiddenError)):
get_logger().error("API error: %s: %s", name, msg) get_logger().error("API error: %s: %s", name, msg)
return _json({ return _json({
"error": name, "error": name,
@ -54,25 +82,36 @@ def _json_exception(err: Exception, status: int) -> aiohttp.web.Response:
}, status=status) }, status=status)
class BadRequestError(Exception):
pass
_ATTR_EXPOSED = "exposed" _ATTR_EXPOSED = "exposed"
_ATTR_EXPOSED_METHOD = "exposed_method" _ATTR_EXPOSED_METHOD = "exposed_method"
_ATTR_EXPOSED_PATH = "exposed_path" _ATTR_EXPOSED_PATH = "exposed_path"
_ATTR_SYSTEM_TASK = "system_task" _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: def make_wrapper(method: Callable) -> Callable:
async def wrap(self: "Server", request: aiohttp.web.Request) -> aiohttp.web.Response: async def wrap(self: "Server", request: aiohttp.web.Request) -> aiohttp.web.Response:
try: 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)) return (await method(self, request))
except RegionIsBusyError as err: except RegionIsBusyError as err:
return _json_exception(err, 409) return _json_exception(err, 409)
except (BadRequestError, MsdOperationError) as err: except (BadRequestError, MsdOperationError) as err:
return _json_exception(err, 400) 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, True)
setattr(wrap, _ATTR_EXPOSED_METHOD, http_method) setattr(wrap, _ATTR_EXPOSED_METHOD, http_method)
@ -95,6 +134,29 @@ def _system_task(method: Callable) -> Callable:
return wrap 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: def _valid_bool(name: str, flag: Optional[str]) -> bool:
flag = str(flag).strip().lower() flag = str(flag).strip().lower()
if flag in ["1", "true", "yes"]: if flag in ["1", "true", "yes"]:
@ -127,6 +189,7 @@ class _Events(Enum):
class Server: # pylint: disable=too-many-instance-attributes class Server: # pylint: disable=too-many-instance-attributes
def __init__( # pylint: disable=too-many-arguments def __init__( # pylint: disable=too-many-arguments
self, self,
auth_manager: AuthManager,
info_manager: InfoManager, info_manager: InfoManager,
log_reader: LogReader, log_reader: LogReader,
@ -142,6 +205,7 @@ class Server: # pylint: disable=too-many-instance-attributes
loop: asyncio.AbstractEventLoop, loop: asyncio.AbstractEventLoop,
) -> None: ) -> None:
self._auth_manager = auth_manager
self.__info_manager = info_manager self.__info_manager = info_manager
self.__log_reader = log_reader self.__log_reader = log_reader
@ -210,6 +274,29 @@ class Server: # pylint: disable=too-many-instance-attributes
"extras": await self.__info_manager.get_extras(), "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 # ===== SYSTEM
@_exposed("GET", "/info") @_exposed("GET", "/info")

View File

@ -31,12 +31,7 @@ RUN useradd -r -d / packer \
&& cd - \ && cd - \
&& rm -rf /tmp/packer-color && 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 \ RUN pacman -Syy \
&& user-packer -S --noconfirm \
customizepkg \
&& mkdir /.npm \ && mkdir /.npm \
&& chmod 777 /.npm \ && chmod 777 /.npm \
&& user-packer -S --noconfirm \ && user-packer -S --noconfirm \
@ -50,7 +45,6 @@ RUN pacman -Syy \
htmlhint \ htmlhint \
eslint \ eslint \
&& rm -rf /.npm \ && rm -rf /.npm \
&& env MAKEPKGOPTS="--skipchecksums --skippgpcheck" user-packer -S --noconfirm nginx-mainline-mod-lua \
&& pacman -Sc --noconfirm && pacman -Sc --noconfirm
COPY testenv/requirements.txt requirements.txt 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 port: 8081
heartbeat: 3.0 heartbeat: 3.0
auth:
htpasswd: /etc/kvmd/htpasswd
info: info:
meta: /etc/kvmd/meta.yaml meta: /etc/kvmd/meta.yaml
extras: /usr/share/kvmd/extras extras: /usr/share/kvmd/extras

View File

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

View File

@ -33,7 +33,7 @@ deps =
[testenv:eslint] [testenv:eslint]
whitelist_externals = 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] [testenv:htmlhint]
whitelist_externals = 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" /> <meta charset="utf-8" />
<title>Pi-KVM Index</title> <title>Pi-KVM Index</title>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="/share/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/share/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/share/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest"> <link rel="manifest" href="/share/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5"> <link rel="mask-icon" href="/share/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#2b5797"> <meta name="msapplication-TileColor" content="#2b5797">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="css/vars.css"> <link rel="stylesheet" href="share/css/vars.css">
<link rel="stylesheet" href="css/main.css"> <link rel="stylesheet" href="share/css/main.css">
<link rel="stylesheet" href="css/modals.css"> <link rel="stylesheet" href="share/css/modals.css">
<link rel="stylesheet" href="css/index/index.css"> <link rel="stylesheet" href="share/css/index/index.css">
<script src="js/bb.js"></script> <script src="share/js/bb.js"></script>
<script src="js/tools.js"></script> <script src="share/js/tools.js"></script>
<script src="js/index/main.js"></script> <script src="share/js/index/main.js"></script>
<script>window.onload = main;</script> <script>window.onload = main;</script>
</head> </head>
@ -30,7 +30,7 @@
<table> <table>
<tr> <tr>
<td valign="top" class="logo"> <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>
<td valign="top"> <td valign="top">
<table> <table>

View File

@ -4,40 +4,40 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Pi-KVM Session</title> <title>Pi-KVM Session</title>
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png"> <link rel="apple-touch-icon" sizes="180x180" href="/share/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png"> <link rel="icon" type="image/png" sizes="32x32" href="/share/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png"> <link rel="icon" type="image/png" sizes="16x16" href="/share/favicon-16x16.png">
<link rel="manifest" href="/site.webmanifest"> <link rel="manifest" href="/share/site.webmanifest">
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5"> <link rel="mask-icon" href="/share/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#2b5797"> <meta name="msapplication-TileColor" content="#2b5797">
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="../css/vars.css"> <link rel="stylesheet" href="../share/css/vars.css">
<link rel="stylesheet" href="../css/main.css"> <link rel="stylesheet" href="../share/css/main.css">
<link rel="stylesheet" href="../css/menu.css"> <link rel="stylesheet" href="../share/css/menu.css">
<link rel="stylesheet" href="../css/windows.css"> <link rel="stylesheet" href="../share/css/windows.css">
<link rel="stylesheet" href="../css/modals.css"> <link rel="stylesheet" href="../share/css/modals.css">
<link rel="stylesheet" href="../css/leds.css"> <link rel="stylesheet" href="../share/css/leds.css">
<link rel="stylesheet" href="../css/sliders.css"> <link rel="stylesheet" href="../share/css/sliders.css">
<link rel="stylesheet" href="../css/switches.css"> <link rel="stylesheet" href="../share/css/switches.css">
<link rel="stylesheet" href="../css/progress.css"> <link rel="stylesheet" href="../share/css/progress.css">
<link rel="stylesheet" href="../css/kvm/stream.css"> <link rel="stylesheet" href="../share/css/kvm/stream.css">
<link rel="stylesheet" href="../css/kvm/hid.css"> <link rel="stylesheet" href="../share/css/kvm/hid.css">
<link rel="stylesheet" href="../css/kvm/msd.css"> <link rel="stylesheet" href="../share/css/kvm/msd.css">
<link rel="stylesheet" href="../css/kvm/keyboard.css"> <link rel="stylesheet" href="../share/css/kvm/keyboard.css">
<link rel="stylesheet" href="../css/kvm/about.css"> <link rel="stylesheet" href="../share/css/kvm/about.css">
<script src="../js/bb.js"></script> <script src="../share/js/bb.js"></script>
<script src="../js/tools.js"></script> <script src="../share/js/tools.js"></script>
<script src="../js/wm.js"></script> <script src="../share/js/wm.js"></script>
<script src="../js/kvm/stream.js"></script> <script src="../share/js/kvm/stream.js"></script>
<script src="../js/kvm/atx.js"></script> <script src="../share/js/kvm/atx.js"></script>
<script src="../js/kvm/keyboard.js"></script> <script src="../share/js/kvm/keyboard.js"></script>
<script src="../js/kvm/mouse.js"></script> <script src="../share/js/kvm/mouse.js"></script>
<script src="../js/kvm/hid.js"></script> <script src="../share/js/kvm/hid.js"></script>
<script src="../js/kvm/msd.js"></script> <script src="../share/js/kvm/msd.js"></script>
<script src="../js/kvm/session.js"></script> <script src="../share/js/kvm/session.js"></script>
<script src="../js/kvm/main.js"></script> <script src="../share/js/kvm/main.js"></script>
<script>window.onload = main;</script> <script>window.onload = main;</script>
</head> </head>
@ -47,16 +47,16 @@
<li class="menu-left-items"> <li class="menu-left-items">
<a id="menu-logo" href="/"> <a id="menu-logo" href="/">
&#8617;&nbsp;&nbsp; &#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> </a>
</li> </li>
<li class="menu-right-items"> <li class="menu-right-items">
<a class="menu-item" href="#"> <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="link-led" class="led-gray" src="../share/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="stream-led" class="led-gray" src="../share/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-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="../svg/hid-mouse-led.svg" /> <img data-dont-hide-menu id="hid-mouse-led" class="led-gray" src="../share/svg/hid-mouse-led.svg" />
System &#8628; System &#8628;
</a> </a>
<div data-dont-hide-menu class="menu-item-content"> <div data-dont-hide-menu class="menu-item-content">
@ -118,8 +118,8 @@
<li class="menu-right-items"> <li class="menu-right-items">
<a class="menu-item" href="#"> <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-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="../svg/atx-hdd-led.svg" /> <img data-dont-hide-menu id="atx-hdd-led" class="led-gray" src="../share/svg/atx-hdd-led.svg" />
ATX &#8628; ATX &#8628;
</a> </a>
<div class="menu-item-content menu-item-content-buttons"> <div class="menu-item-content menu-item-content-buttons">
@ -132,7 +132,7 @@
<li class="menu-right-items"> <li class="menu-right-items">
<a class="menu-item" href="#"> <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; Mass Storage &#8628;
</a> </a>
<div data-dont-hide-menu id="msd-menu" class="menu-item-content"> <div data-dont-hide-menu id="msd-menu" class="menu-item-content">
@ -140,7 +140,7 @@
<div class="menu-item-content-text"> <div class="menu-item-content-text">
<table> <table>
<tr> <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> <td><b>Mass Storage Device is not operational</b></td>
</tr> </tr>
</table> </table>
@ -152,7 +152,7 @@
<div class="menu-item-content-text"> <div class="menu-item-content-text">
<table> <table>
<tr> <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> <td><b>Current image is broken!</b><br><sub>Perhaps uploading was interrupted</sub></td>
</tr> </tr>
</table> </table>
@ -164,7 +164,7 @@
<div class="menu-item-content-text"> <div class="menu-item-content-text">
<table> <table>
<tr> <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> <td><b>Another user uploads an image</b></td>
</tr> </tr>
</table> </table>
@ -233,7 +233,7 @@
<li class="menu-right-items"> <li class="menu-right-items">
<a class="menu-item" href="#"> <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; Shortcuts &#8628;
</a> </a>
<div data-dont-hide-menu class="menu-item-content"> <div data-dont-hide-menu class="menu-item-content">
@ -282,7 +282,7 @@
</div> </div>
<div id="stream-info"></div> <div id="stream-info"></div>
<div id="stream-box" class="stream-box-inactive"> <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>
<div id="stream-mouse-buttons"> <div id="stream-mouse-buttons">
<button data-mouse-button="left" class="row50">Left Click</button> <button data-mouse-button="left" class="row50">Left Click</button>
@ -534,7 +534,7 @@
<table> <table>
<tr> <tr>
<td valign="top" class="logo"> <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>
<td valign="top"> <td valign="top">
<table> <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 { button, select {
box-shadow: none; box-shadow: none;
border: none; border: none;
border-radius: 4px;
color: var(--cs-control-default-fg); color: var(--cs-control-default-fg);
background-color: var(--cs-control-default-bg); background-color: var(--cs-control-default-bg);
display: block; display: block;
@ -116,8 +117,18 @@ select:active {
background-image: url("../svg/select-arrow-intensive.svg") !important; 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) { @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; 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 { ul#menu li div.menu-item-content button, select {
border-radius: 0;
text-align: left; text-align: left;
padding: 0 16px; 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 { div.modal div.modal-window div.modal-buttons button {
border-radius: 0;
height: 40px; height: 40px;
} }
@media only screen and (min-width: 768px) and (max-width: 1024px) and (orientation: portrait) { @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 __streamer = new Streamer();
var __init__ = function() { var __init__ = function() {
$("link-led").title = "Not connected yet..."; __startSession();
__startPoller();
}; };
/********************************************************************************/ /********************************************************************************/
@ -44,24 +43,15 @@ function Session() {
$("about-version-streamer").innerHTML = `${state.version.streamer} (${state.streamer})`; $("about-version-streamer").innerHTML = `${state.version.streamer} (${state.streamer})`;
}; };
var __startPoller = function() { var __startSession = function() {
$("link-led").className = "led-yellow"; $("link-led").className = "led-yellow";
$("link-led").title = "Connecting..."; $("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"); var proto = (location.protocol === "https:" ? "wss" : "ws");
__ws = new WebSocket(`${proto}://${location.host}/kvmd/ws`); __ws = new WebSocket(`${proto}://${location.host}/kvmd/ws`);
__ws.onopen = __wsOpenHandler; __ws.onopen = __wsOpenHandler;
__ws.onmessage = __wsMessageHandler; __ws.onmessage = __wsMessageHandler;
__ws.onerror = __wsErrorHandler; __ws.onerror = __wsErrorHandler;
__ws.onclose = __wsCloseHandler; __ws.onclose = __wsCloseHandler;
} else {
__wsCloseHandler(null);
}
}
});
}; };
var __wsOpenHandler = function(event) { var __wsOpenHandler = function(event) {
@ -118,7 +108,7 @@ function Session() {
setTimeout(function() { setTimeout(function() {
$("link-led").className = "led-yellow"; $("link-led").className = "led-yellow";
setTimeout(__startPoller, 500); setTimeout(__startSession, 500);
}, 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 tools = new function() {
var __debug = (new URL(window.location.href)).searchParams.get("debug"); 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(); var http = new XMLHttpRequest();
http.open(method, url, true); http.open(method, url, true);
if (content_type) {
http.setRequestHeader("Content-Type", content_type);
}
http.onreadystatechange = callback; http.onreadystatechange = callback;
http.timeout = (timeout ? timeout : 5000); http.timeout = 5000;
http.send(); http.send(body);
return http; 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": "", "short_name": "",
"icons": [ "icons": [
{ {
"src": "/android-chrome-192x192.png", "src": "/share/android-chrome-192x192.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "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