own auth
1
Makefile
@ -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 \
|
||||||
|
|||||||
1
PKGBUILD
@ -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
@ -0,0 +1 @@
|
|||||||
|
admin:$apr1$INC0KeyU$YdLQ9qosXzNVlhxQPUf7A/
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
@ -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)
|
||||||
@ -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")
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -1 +0,0 @@
|
|||||||
replace#global#_nginxver=.*#_nginxver=`pacman -Q nginx-mainline | grep -Po "\\d+\\.\\d+\\.\\d+"`
|
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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>
|
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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="/">
|
||||||
↩
|
↩
|
||||||
<img class="svg-gray" src="../svg/logo.svg" alt="π-kvm" />
|
<img class="svg-gray" src="../share/svg/logo.svg" alt="π-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 ↴
|
System ↴
|
||||||
</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 ↴
|
ATX ↴
|
||||||
</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 ↴
|
Mass Storage ↴
|
||||||
</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 ↴
|
Shortcuts ↴
|
||||||
</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
@ -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>
|
||||||
|
Before Width: | Height: | Size: 3.6 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 6.1 KiB |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
25
web/share/css/login/login.css
Normal 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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
@ -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) {
|
||||||
|
Before Width: | Height: | Size: 638 B After Width: | Height: | Size: 638 B |
|
Before Width: | Height: | Size: 937 B After Width: | Height: | Size: 937 B |
@ -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);
|
||||||
};
|
};
|
||||||
|
|
||||||
36
web/share/js/login/main.js
Normal 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;
|
||||||
|
}
|
||||||
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 339 KiB After Width: | Height: | Size: 339 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
@ -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"
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.1 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.1 KiB |
|
Before Width: | Height: | Size: 5.6 KiB After Width: | Height: | Size: 5.6 KiB |
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 9.8 KiB After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 328 B After Width: | Height: | Size: 328 B |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 1.8 KiB |