qtoggleserver 0.28.0b1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- qtoggleserver/commands/__init__.py +28 -0
- qtoggleserver/commands/server.py +18 -0
- qtoggleserver/commands/shell.py +21 -0
- qtoggleserver/conf/__init__.py +0 -0
- qtoggleserver/conf/settings.py +158 -0
- qtoggleserver/core/__init__.py +0 -0
- qtoggleserver/core/api/__init__.py +111 -0
- qtoggleserver/core/api/auth.py +105 -0
- qtoggleserver/core/api/funcs/__init__.py +0 -0
- qtoggleserver/core/api/funcs/backup.py +26 -0
- qtoggleserver/core/api/funcs/device.py +68 -0
- qtoggleserver/core/api/funcs/firmware.py +47 -0
- qtoggleserver/core/api/funcs/ports.py +471 -0
- qtoggleserver/core/api/funcs/reverse.py +31 -0
- qtoggleserver/core/api/funcs/various.py +99 -0
- qtoggleserver/core/api/funcs/webhooks.py +31 -0
- qtoggleserver/core/api/schema.py +172 -0
- qtoggleserver/core/device/__init__.py +93 -0
- qtoggleserver/core/device/attrs.py +736 -0
- qtoggleserver/core/device/events.py +5 -0
- qtoggleserver/core/events/__init__.py +35 -0
- qtoggleserver/core/events/base.py +86 -0
- qtoggleserver/core/events/device.py +29 -0
- qtoggleserver/core/events/handlers.py +76 -0
- qtoggleserver/core/events/port.py +64 -0
- qtoggleserver/core/expressions/__init__.py +87 -0
- qtoggleserver/core/expressions/aggregation.py +42 -0
- qtoggleserver/core/expressions/arithmetic.py +68 -0
- qtoggleserver/core/expressions/base.py +69 -0
- qtoggleserver/core/expressions/bitwise.py +64 -0
- qtoggleserver/core/expressions/comparison.py +64 -0
- qtoggleserver/core/expressions/date.py +334 -0
- qtoggleserver/core/expressions/exceptions.py +149 -0
- qtoggleserver/core/expressions/functions.py +150 -0
- qtoggleserver/core/expressions/literalvalues.py +58 -0
- qtoggleserver/core/expressions/logic.py +47 -0
- qtoggleserver/core/expressions/ports.py +112 -0
- qtoggleserver/core/expressions/rounding.py +40 -0
- qtoggleserver/core/expressions/sign.py +24 -0
- qtoggleserver/core/expressions/time.py +20 -0
- qtoggleserver/core/expressions/timeprocessing.py +280 -0
- qtoggleserver/core/expressions/various.py +330 -0
- qtoggleserver/core/history.py +245 -0
- qtoggleserver/core/main.py +243 -0
- qtoggleserver/core/ports.py +1256 -0
- qtoggleserver/core/responses.py +163 -0
- qtoggleserver/core/reverse.py +335 -0
- qtoggleserver/core/sequences.py +65 -0
- qtoggleserver/core/sessions.py +140 -0
- qtoggleserver/core/typing.py +15 -0
- qtoggleserver/core/vports.py +102 -0
- qtoggleserver/core/webhooks.py +254 -0
- qtoggleserver/drivers/__init__.py +0 -0
- qtoggleserver/drivers/events/__init__.py +0 -0
- qtoggleserver/drivers/events/dummy.py +79 -0
- qtoggleserver/drivers/fwupdate/__init__.py +4 -0
- qtoggleserver/drivers/fwupdate/dummy.py +57 -0
- qtoggleserver/drivers/persist/__init__.py +26 -0
- qtoggleserver/drivers/persist/json.py +306 -0
- qtoggleserver/drivers/persist/mongo.py +170 -0
- qtoggleserver/drivers/persist/postgresql.py +557 -0
- qtoggleserver/drivers/persist/redis.py +301 -0
- qtoggleserver/drivers/ports/__init__.py +0 -0
- qtoggleserver/drivers/ports/dummynumeric.py +54 -0
- qtoggleserver/drivers/ports/gpio/__init__.py +5 -0
- qtoggleserver/drivers/ports/gpio/dummy.py +54 -0
- qtoggleserver/drivers/ports/gpio/gpio.py +104 -0
- qtoggleserver/frontend/__init__.py +0 -0
- qtoggleserver/frontend/api/__init__.py +0 -0
- qtoggleserver/frontend/api/funcs.py +70 -0
- qtoggleserver/frontend/dist/font/dejavusans-bold.woff +0 -0
- qtoggleserver/frontend/dist/font/dejavusans-bolditalic.woff +0 -0
- qtoggleserver/frontend/dist/font/dejavusans-italic.woff +0 -0
- qtoggleserver/frontend/dist/font/dejavusans-regular.woff +0 -0
- qtoggleserver/frontend/dist/img/launcher-icon-144.png +0 -0
- qtoggleserver/frontend/dist/img/launcher-icon-16.png +0 -0
- qtoggleserver/frontend/dist/img/launcher-icon-192.png +0 -0
- qtoggleserver/frontend/dist/img/launcher-icon-256.png +0 -0
- qtoggleserver/frontend/dist/img/launcher-icon-32.png +0 -0
- qtoggleserver/frontend/dist/img/launcher-icon-36.png +0 -0
- qtoggleserver/frontend/dist/img/launcher-icon-384.png +0 -0
- qtoggleserver/frontend/dist/img/launcher-icon-48.png +0 -0
- qtoggleserver/frontend/dist/img/launcher-icon-512.png +0 -0
- qtoggleserver/frontend/dist/img/launcher-icon-64.png +0 -0
- qtoggleserver/frontend/dist/img/launcher-icon-72.png +0 -0
- qtoggleserver/frontend/dist/img/launcher-icon-96.png +0 -0
- qtoggleserver/frontend/dist/img/launcher-icon.svg +218 -0
- qtoggleserver/frontend/dist/img/qtoggle-icons.svg +1561 -0
- qtoggleserver/frontend/dist/img/qui-icons.svg +1937 -0
- qtoggleserver/frontend/dist/qtoggleserver-bundle-dark.css +1 -0
- qtoggleserver/frontend/dist/qtoggleserver-bundle-light.css +1 -0
- qtoggleserver/frontend/dist/qtoggleserver-bundle.js +3 -0
- qtoggleserver/frontend/dist/qtoggleserver-bundle.js.LICENSE.txt +259 -0
- qtoggleserver/frontend/dist/qtoggleserver-bundle.js.map +1 -0
- qtoggleserver/frontend/dist/templates/index.html +35 -0
- qtoggleserver/frontend/events.py +34 -0
- qtoggleserver/lib/__init__.py +0 -0
- qtoggleserver/lib/ble.py +370 -0
- qtoggleserver/lib/filtereventhandler.py +450 -0
- qtoggleserver/lib/onewire.py +96 -0
- qtoggleserver/lib/polled.py +182 -0
- qtoggleserver/lib/templatenotifications.py +260 -0
- qtoggleserver/peripherals/__init__.py +90 -0
- qtoggleserver/peripherals/api/__init__.py +0 -0
- qtoggleserver/peripherals/api/funcs.py +81 -0
- qtoggleserver/peripherals/api/schema.py +31 -0
- qtoggleserver/peripherals/exceptions.py +14 -0
- qtoggleserver/peripherals/peripheral.py +259 -0
- qtoggleserver/peripherals/peripheralport.py +44 -0
- qtoggleserver/persist/__init__.py +382 -0
- qtoggleserver/persist/base.py +196 -0
- qtoggleserver/persist/typing.py +7 -0
- qtoggleserver/slaves/__init__.py +36 -0
- qtoggleserver/slaves/api/__init__.py +0 -0
- qtoggleserver/slaves/api/funcs/__init__.py +0 -0
- qtoggleserver/slaves/api/funcs/devices.py +349 -0
- qtoggleserver/slaves/api/funcs/discovered.py +48 -0
- qtoggleserver/slaves/api/schema.py +41 -0
- qtoggleserver/slaves/devices.py +1671 -0
- qtoggleserver/slaves/discover/__init__.py +27 -0
- qtoggleserver/slaves/discover/apclients.py +344 -0
- qtoggleserver/slaves/discover/exceptions.py +6 -0
- qtoggleserver/slaves/events.py +49 -0
- qtoggleserver/slaves/exceptions.py +77 -0
- qtoggleserver/slaves/ports.py +428 -0
- qtoggleserver/slaves/utils.py +9 -0
- qtoggleserver/startup.py +369 -0
- qtoggleserver/system/__init__.py +66 -0
- qtoggleserver/system/ap/__init__.py +132 -0
- qtoggleserver/system/ap/client.py +72 -0
- qtoggleserver/system/ap/dnsmasq.py +192 -0
- qtoggleserver/system/ap/exceptions.py +2 -0
- qtoggleserver/system/ap/hostapd.py +155 -0
- qtoggleserver/system/api/__init__.py +0 -0
- qtoggleserver/system/api/funcs.py +21 -0
- qtoggleserver/system/battery.py +43 -0
- qtoggleserver/system/conf.py +51 -0
- qtoggleserver/system/date.py +55 -0
- qtoggleserver/system/dhcp.py +285 -0
- qtoggleserver/system/dns.py +54 -0
- qtoggleserver/system/fwupdate.py +125 -0
- qtoggleserver/system/net.py +123 -0
- qtoggleserver/system/storage.py +14 -0
- qtoggleserver/system/temperature.py +33 -0
- qtoggleserver/utils/__init__.py +0 -0
- qtoggleserver/utils/asyncio.py +164 -0
- qtoggleserver/utils/cmd.py +78 -0
- qtoggleserver/utils/conf.py +54 -0
- qtoggleserver/utils/dynload.py +20 -0
- qtoggleserver/utils/json.py +163 -0
- qtoggleserver/utils/logging.py +46 -0
- qtoggleserver/utils/timedset.py +35 -0
- qtoggleserver/version.py +2 -0
- qtoggleserver/web/__init__.py +42 -0
- qtoggleserver/web/base.py +191 -0
- qtoggleserver/web/handlers.py +243 -0
- qtoggleserver/web/server.py +177 -0
- qtoggleserver-0.28.0b1.dist-info/METADATA +37 -0
- qtoggleserver-0.28.0b1.dist-info/RECORD +163 -0
- qtoggleserver-0.28.0b1.dist-info/WHEEL +5 -0
- qtoggleserver-0.28.0b1.dist-info/entry_points.txt +3 -0
- qtoggleserver-0.28.0b1.dist-info/licenses/LICENSE.txt +177 -0
- qtoggleserver-0.28.0b1.dist-info/top_level.txt +3 -0
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from collections.abc import Awaitable, Callable
|
|
4
|
+
|
|
5
|
+
from qtoggleserver import startup
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def execute(main_func: Callable[[], Awaitable[bool]]) -> None:
|
|
9
|
+
loop = asyncio.new_event_loop()
|
|
10
|
+
|
|
11
|
+
loop.run_until_complete(startup.init_loop())
|
|
12
|
+
loop.run_until_complete(startup.init())
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
run_loop = loop.run_until_complete(main_func())
|
|
16
|
+
if run_loop:
|
|
17
|
+
loop.run_forever()
|
|
18
|
+
loop.run_until_complete(startup.cleanup())
|
|
19
|
+
|
|
20
|
+
finally:
|
|
21
|
+
try:
|
|
22
|
+
loop.run_until_complete(startup.cleanup_loop())
|
|
23
|
+
except asyncio.CancelledError:
|
|
24
|
+
pass # ignore any cancelled errors
|
|
25
|
+
|
|
26
|
+
loop.close()
|
|
27
|
+
|
|
28
|
+
startup.logger.info("bye!")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
from qtoggleserver import commands
|
|
4
|
+
from qtoggleserver.web import server as web_server
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def main() -> bool:
|
|
8
|
+
await web_server.init()
|
|
9
|
+
|
|
10
|
+
return True # run loop afterwards
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def execute() -> None:
|
|
14
|
+
commands.execute(main)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
execute()
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
|
|
3
|
+
import code
|
|
4
|
+
|
|
5
|
+
from qtoggleserver import commands
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
async def main() -> bool:
|
|
9
|
+
import qtoggleserver # noqa: F401 - Required for locals()
|
|
10
|
+
|
|
11
|
+
code.interact(local=locals())
|
|
12
|
+
|
|
13
|
+
return False # don't run loop afterwards
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def execute() -> None:
|
|
17
|
+
commands.execute(main)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
execute()
|
|
File without changes
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from typing import Any as _Any
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
source: str | None = None # full path to the configuration file, automatically set at startup
|
|
5
|
+
|
|
6
|
+
debug: bool = False
|
|
7
|
+
|
|
8
|
+
public_url: str | None = None
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
logging: dict[str, _Any] = {
|
|
12
|
+
"version": 1,
|
|
13
|
+
"memory_logs_buffer_len": 10000,
|
|
14
|
+
"formatters": {
|
|
15
|
+
"default": {"format": "%(asctime)s: %(levelname)7s: [%(name)s] %(message)s", "datefmt": "%Y-%m-%d %H:%M:%S"}
|
|
16
|
+
},
|
|
17
|
+
"handlers": {"console": {"class": "logging.StreamHandler", "formatter": "default"}},
|
|
18
|
+
"loggers": {
|
|
19
|
+
# Double quotes are necessary to avoid HOCON key split
|
|
20
|
+
'"asyncio"': {"level": "INFO"},
|
|
21
|
+
'"bleak"': {"level": "INFO"},
|
|
22
|
+
'"qtoggleserver.core.sessions"': {"level": "INFO"},
|
|
23
|
+
'"qtoggleserver.persist"': {"level": "INFO"},
|
|
24
|
+
'"qtoggleserver.drivers.persist"': {"level": "INFO"},
|
|
25
|
+
'"qtoggleserver.utils.cmd"': {"level": "INFO"},
|
|
26
|
+
},
|
|
27
|
+
"root": {"level": "DEBUG", "handlers": ["console"]},
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class core:
|
|
32
|
+
class device_name:
|
|
33
|
+
get_cmd: str | None = None
|
|
34
|
+
set_cmd: str | None = None
|
|
35
|
+
|
|
36
|
+
class passwords:
|
|
37
|
+
set_cmd: str | None = None
|
|
38
|
+
|
|
39
|
+
tick_interval: int = 50
|
|
40
|
+
persist_interval: int = 2000
|
|
41
|
+
event_queue_size: int = 1024
|
|
42
|
+
max_client_time_skew: int = 300
|
|
43
|
+
backup_support: bool = True
|
|
44
|
+
history_support: bool = True
|
|
45
|
+
history_janitor_interval: int = 3600
|
|
46
|
+
listen_support: bool = True
|
|
47
|
+
sequences_support: bool = True
|
|
48
|
+
tls_support: bool = True
|
|
49
|
+
virtual_ports: int = 1024
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class server:
|
|
53
|
+
addr: str = "0.0.0.0"
|
|
54
|
+
port: int = 8888
|
|
55
|
+
compress_response: bool = True
|
|
56
|
+
|
|
57
|
+
class https:
|
|
58
|
+
cert_file: str | None = None
|
|
59
|
+
key_file: str | None = None
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class persist:
|
|
63
|
+
driver: str = "qtoggleserver.drivers.persist.JSONDriver"
|
|
64
|
+
file_path: str = "qtoggleserver-data.json"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class system:
|
|
68
|
+
setup_mode_cmd: str | None = None
|
|
69
|
+
|
|
70
|
+
class date:
|
|
71
|
+
set_cmd: str | None = None
|
|
72
|
+
set_format: str | None = "%Y-%m-%dT%H:%M:%SZ"
|
|
73
|
+
|
|
74
|
+
class timezone:
|
|
75
|
+
get_cmd: str | None = None
|
|
76
|
+
set_cmd: str | None = None
|
|
77
|
+
|
|
78
|
+
class net:
|
|
79
|
+
class wifi:
|
|
80
|
+
get_cmd: str | None = None
|
|
81
|
+
set_cmd: str | None = None
|
|
82
|
+
|
|
83
|
+
class ip:
|
|
84
|
+
get_cmd: str | None = None
|
|
85
|
+
set_cmd: str | None = None
|
|
86
|
+
|
|
87
|
+
class storage:
|
|
88
|
+
path: str | None = None
|
|
89
|
+
|
|
90
|
+
class temperature:
|
|
91
|
+
get_cmd: str | None = None
|
|
92
|
+
sensor_name: str | None = None
|
|
93
|
+
sensor_index: int = 0
|
|
94
|
+
min: int | None = 0
|
|
95
|
+
max: int | None = 100
|
|
96
|
+
|
|
97
|
+
class battery:
|
|
98
|
+
get_cmd: str | None = None
|
|
99
|
+
|
|
100
|
+
class fwupdate:
|
|
101
|
+
driver: str | None = None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class frontend:
|
|
105
|
+
enabled: bool = True
|
|
106
|
+
debug: bool = False
|
|
107
|
+
static_url: str | None = None
|
|
108
|
+
display_name: str = "qToggleServer"
|
|
109
|
+
display_short_name: str = "qToggleServer"
|
|
110
|
+
description: str = "An application to control qToggleServer" # TODO: i18n
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class slaves:
|
|
114
|
+
enabled: bool = True
|
|
115
|
+
timeout: int = 10
|
|
116
|
+
long_timeout: int = 60
|
|
117
|
+
keepalive: int = 10
|
|
118
|
+
retry_interval: int = 5
|
|
119
|
+
retry_count: int = 3
|
|
120
|
+
|
|
121
|
+
class discover:
|
|
122
|
+
request_timeout: int = 5
|
|
123
|
+
dhcp_timeout: int = 10
|
|
124
|
+
dhcp_interface: str | None = None
|
|
125
|
+
|
|
126
|
+
class ap:
|
|
127
|
+
interface: str | None = None
|
|
128
|
+
interface_cmd: str | None = None
|
|
129
|
+
ssid: str = "qToggleSetup"
|
|
130
|
+
psk: str | None = None
|
|
131
|
+
own_ip: str = "192.168.43.1"
|
|
132
|
+
mask_len: int = 24
|
|
133
|
+
start_ip: str = "192.168.43.50"
|
|
134
|
+
stop_ip: str = "192.168.43.250"
|
|
135
|
+
hostapd_binary: str | None = None
|
|
136
|
+
hostapd_cli_binary: str | None = None
|
|
137
|
+
dnsmasq_binary: str | None = None
|
|
138
|
+
hostapd_log: str = "/tmp/hostapd.log"
|
|
139
|
+
dnsmasq_log: str = "/tmp/dnsmasq.log"
|
|
140
|
+
finish_timeout: int = 300
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class webhooks:
|
|
144
|
+
enabled: bool = False
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class reverse:
|
|
148
|
+
enabled: bool = False
|
|
149
|
+
retry_interval: int = 5
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
event_handlers: list[dict[str, _Any]] = []
|
|
153
|
+
|
|
154
|
+
peripherals: list[dict[str, _Any]] = []
|
|
155
|
+
|
|
156
|
+
ports: list[dict[str, _Any]] = []
|
|
157
|
+
|
|
158
|
+
port_mappings: dict[str, str] = {}
|
|
File without changes
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from qtoggleserver.core import responses as core_responses
|
|
10
|
+
from qtoggleserver.core.typing import GenericJSONDict
|
|
11
|
+
from qtoggleserver.web import APIHandler
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
API_VERSION = "1.1"
|
|
15
|
+
|
|
16
|
+
ACCESS_LEVEL_ADMIN = 30
|
|
17
|
+
ACCESS_LEVEL_NORMAL = 20
|
|
18
|
+
ACCESS_LEVEL_VIEWONLY = 10
|
|
19
|
+
ACCESS_LEVEL_NONE = 0
|
|
20
|
+
|
|
21
|
+
ACCESS_LEVEL_MAPPING = {
|
|
22
|
+
ACCESS_LEVEL_ADMIN: "admin",
|
|
23
|
+
ACCESS_LEVEL_NORMAL: "normal",
|
|
24
|
+
ACCESS_LEVEL_VIEWONLY: "viewonly",
|
|
25
|
+
ACCESS_LEVEL_NONE: "none",
|
|
26
|
+
"admin": ACCESS_LEVEL_ADMIN,
|
|
27
|
+
"normal": ACCESS_LEVEL_NORMAL,
|
|
28
|
+
"viewonly": ACCESS_LEVEL_VIEWONLY,
|
|
29
|
+
"none": ACCESS_LEVEL_NONE,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
logger = logging.getLogger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class APIError(Exception):
|
|
36
|
+
def __init__(self, status: int, code: str, **params) -> None:
|
|
37
|
+
self.status: int = status
|
|
38
|
+
self.code: str = code
|
|
39
|
+
self.params: dict = params
|
|
40
|
+
|
|
41
|
+
super().__init__(code)
|
|
42
|
+
|
|
43
|
+
@staticmethod
|
|
44
|
+
def from_http_error(http_error: core_responses.HTTPError) -> APIError:
|
|
45
|
+
return APIError(http_error.status, http_error.code, **http_error.params)
|
|
46
|
+
|
|
47
|
+
def to_json(self) -> GenericJSONDict:
|
|
48
|
+
return dict(error=self.code, **self.params)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class APIAccepted(Exception):
|
|
52
|
+
def __init__(self, response: Any = None) -> None:
|
|
53
|
+
self.response: Any = response
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class APIRequest:
|
|
57
|
+
def __init__(self, handler: APIHandler) -> None:
|
|
58
|
+
self.handler: APIHandler = handler
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def access_level(self) -> int:
|
|
62
|
+
return self.handler.access_level
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def username(self) -> str:
|
|
66
|
+
return self.handler.username
|
|
67
|
+
|
|
68
|
+
@property
|
|
69
|
+
def session_id(self) -> str | None:
|
|
70
|
+
return self.handler.request.headers.get("Session-Id")
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def method(self) -> str:
|
|
74
|
+
return self.handler.request.method
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def path(self) -> str:
|
|
78
|
+
return self.handler.request.path
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def query(self) -> dict[str, str]:
|
|
82
|
+
return {k: self.handler.decode_argument(v[0]) for k, v in self.handler.request.query_arguments.items()}
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def headers(self) -> dict[str, str]:
|
|
86
|
+
return self.handler.request.headers
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def body(self) -> bytes:
|
|
90
|
+
return self.handler.request.body
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def api_call(access_level: int = ACCESS_LEVEL_NONE) -> Callable:
|
|
94
|
+
def decorator(func: Callable) -> Callable:
|
|
95
|
+
@functools.wraps(func)
|
|
96
|
+
def wrapper(request_handler: APIHandler, *args, **kwargs) -> Any:
|
|
97
|
+
logger.debug('executing API call "%s"', func.__name__)
|
|
98
|
+
|
|
99
|
+
if request_handler.access_level < access_level:
|
|
100
|
+
if request_handler.access_level == ACCESS_LEVEL_NONE: # indicates missing or invalid auth data
|
|
101
|
+
raise APIError(401, "authentication-required")
|
|
102
|
+
else:
|
|
103
|
+
raise APIError(403, "forbidden", required_level=ACCESS_LEVEL_MAPPING.get(access_level))
|
|
104
|
+
|
|
105
|
+
request = APIRequest(request_handler)
|
|
106
|
+
|
|
107
|
+
return func(request, *args, **kwargs)
|
|
108
|
+
|
|
109
|
+
return wrapper
|
|
110
|
+
|
|
111
|
+
return decorator
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import logging
|
|
3
|
+
import re
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
from collections.abc import Callable
|
|
7
|
+
|
|
8
|
+
import jwt
|
|
9
|
+
|
|
10
|
+
from qtoggleserver import system
|
|
11
|
+
from qtoggleserver.conf import settings
|
|
12
|
+
from qtoggleserver.core.device import attrs as core_device_attrs
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
JWT_ISS = "qToggle"
|
|
16
|
+
JWT_ALG = "HS256"
|
|
17
|
+
|
|
18
|
+
ORIGIN_DEVICE = "device"
|
|
19
|
+
ORIGIN_CONSUMER = "consumer"
|
|
20
|
+
|
|
21
|
+
EMPTY_PASSWORD_HASH = hashlib.sha256(b"").hexdigest()
|
|
22
|
+
|
|
23
|
+
_AUTH_TOKEN_RE = re.compile(r"^Bearer\s+([a-z0-9_.-]+)$", re.IGNORECASE)
|
|
24
|
+
|
|
25
|
+
logger = logging.getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class AuthError(Exception):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def make_auth_header(origin: str, username: str | None, password_hash: str) -> str:
|
|
33
|
+
claims: dict[str, str | int] = {"iss": JWT_ISS, "ori": origin}
|
|
34
|
+
|
|
35
|
+
if username:
|
|
36
|
+
claims["usr"] = username
|
|
37
|
+
|
|
38
|
+
if system.date.has_real_date_time():
|
|
39
|
+
claims["iat"] = int(time.time())
|
|
40
|
+
|
|
41
|
+
token = jwt.encode(claims, key=password_hash or "", algorithm=JWT_ALG)
|
|
42
|
+
|
|
43
|
+
return f"Bearer {token}"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def parse_auth_header(auth: str, origin: str, password_hash_func: Callable, require_usr: bool = True) -> str:
|
|
47
|
+
m = _AUTH_TOKEN_RE.match(auth)
|
|
48
|
+
if not m:
|
|
49
|
+
raise AuthError("Invalid authorization header")
|
|
50
|
+
|
|
51
|
+
# Decode but don't validate token yet
|
|
52
|
+
token = m.group(1)
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
payload = jwt.decode(
|
|
56
|
+
token, algorithms=[JWT_ALG], options={"verify_signature": False}, leeway=settings.core.max_client_time_skew
|
|
57
|
+
)
|
|
58
|
+
except jwt.exceptions.InvalidTokenError as e:
|
|
59
|
+
raise AuthError(f"Invalid JWT: {e}") from e
|
|
60
|
+
|
|
61
|
+
# Validate claims
|
|
62
|
+
if payload.get("iss") != JWT_ISS:
|
|
63
|
+
raise AuthError("Missing or invalid iss claim in JWT")
|
|
64
|
+
|
|
65
|
+
if payload.get("ori") != origin:
|
|
66
|
+
raise AuthError("Missing or invalid ori claim in JWT")
|
|
67
|
+
|
|
68
|
+
iat = payload.get("iat")
|
|
69
|
+
if (iat is not None) and system.date.has_real_date_time():
|
|
70
|
+
delta = time.time() - iat
|
|
71
|
+
if abs(delta) > settings.core.max_client_time_skew:
|
|
72
|
+
raise AuthError("JWT too old or too much in the future")
|
|
73
|
+
|
|
74
|
+
usr = payload.get("usr")
|
|
75
|
+
if require_usr:
|
|
76
|
+
if not usr or not isinstance(usr, str):
|
|
77
|
+
raise AuthError("Missing or invalid usr claim in JWT")
|
|
78
|
+
|
|
79
|
+
# Validate username & signature
|
|
80
|
+
password_hash = password_hash_func(usr)
|
|
81
|
+
if not password_hash:
|
|
82
|
+
raise AuthError(f"Unknown usr in JWT: {usr}")
|
|
83
|
+
|
|
84
|
+
# Decode again to verify signature
|
|
85
|
+
try:
|
|
86
|
+
jwt.decode(
|
|
87
|
+
token, key=password_hash, algorithms=[JWT_ALG], verify=True, leeway=settings.core.max_client_time_skew
|
|
88
|
+
)
|
|
89
|
+
except jwt.exceptions.InvalidSignatureError as e:
|
|
90
|
+
raise AuthError("Invalid JWT signature") from e
|
|
91
|
+
except jwt.exceptions.InvalidTokenError as e:
|
|
92
|
+
raise AuthError(f"Invalid JWT: {e}") from e
|
|
93
|
+
|
|
94
|
+
return usr
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def consumer_password_hash_func(usr: str) -> str | None:
|
|
98
|
+
if usr == "admin":
|
|
99
|
+
return core_device_attrs.admin_password_hash
|
|
100
|
+
elif usr == "normal":
|
|
101
|
+
return core_device_attrs.normal_password_hash
|
|
102
|
+
elif usr == "viewonly":
|
|
103
|
+
return core_device_attrs.viewonly_password_hash
|
|
104
|
+
else:
|
|
105
|
+
return None
|
|
File without changes
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from qtoggleserver.conf import settings
|
|
4
|
+
from qtoggleserver.core import api as core_api
|
|
5
|
+
from qtoggleserver.core.typing import GenericJSONList
|
|
6
|
+
from qtoggleserver.system import conf as system_conf
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@core_api.api_call(core_api.ACCESS_LEVEL_ADMIN)
|
|
13
|
+
async def get_backup_endpoints(request: core_api.APIRequest) -> GenericJSONList:
|
|
14
|
+
endpoints = [{"path": "/peripherals", "display_name": "Peripherals", "restore_method": "PUT", "order": 15}]
|
|
15
|
+
|
|
16
|
+
if system_conf.can_write_conf_file():
|
|
17
|
+
endpoints.append(
|
|
18
|
+
{"path": "/system", "display_name": "System Configuration", "restore_method": "PUT", "order": 5}
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if settings.frontend.enabled:
|
|
22
|
+
endpoints.append(
|
|
23
|
+
{"path": "/frontend", "display_name": "App Configuration", "restore_method": "PUT", "order": 45}
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return endpoints
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
from qtoggleserver import system
|
|
2
|
+
from qtoggleserver.core import api as core_api
|
|
3
|
+
from qtoggleserver.core import device as core_device
|
|
4
|
+
from qtoggleserver.core import main
|
|
5
|
+
from qtoggleserver.core.api import schema as core_api_schema
|
|
6
|
+
from qtoggleserver.core.device import attrs as core_device_attrs
|
|
7
|
+
from qtoggleserver.core.device import events as core_device_events
|
|
8
|
+
from qtoggleserver.core.typing import Attributes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@core_api.api_call(core_api.ACCESS_LEVEL_ADMIN)
|
|
12
|
+
async def get_device(request: core_api.APIRequest) -> Attributes:
|
|
13
|
+
return await core_device_attrs.to_json()
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@core_api.api_call(core_api.ACCESS_LEVEL_ADMIN)
|
|
17
|
+
async def put_device(request: core_api.APIRequest, params: Attributes) -> None:
|
|
18
|
+
core_api_schema.validate(params, core_device_attrs.get_schema(loose=True))
|
|
19
|
+
|
|
20
|
+
# Password fields must explicitly be ignored, so we pop them from supplied data
|
|
21
|
+
for f in ("admin", "normal", "viewonly"):
|
|
22
|
+
params.pop(f"{f}_password", None)
|
|
23
|
+
|
|
24
|
+
# Ignore the date attribute
|
|
25
|
+
params.pop("date", None)
|
|
26
|
+
|
|
27
|
+
# Reset device attributes
|
|
28
|
+
await core_device.reset(preserve_attrs=["admin_password_hash", "normal_password_hash", "viewonly_password_hash"])
|
|
29
|
+
await core_device.load()
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
await core_device_attrs.set_attrs(params, ignore_extra=True)
|
|
33
|
+
except core_device_attrs.DeviceAttributeError as e:
|
|
34
|
+
raise core_api.APIError(400, e.error, attribute=e.attribute)
|
|
35
|
+
except Exception as e:
|
|
36
|
+
raise core_api.APIError(500, "unexpected-error", message=str(e)) from e
|
|
37
|
+
|
|
38
|
+
await core_device.save()
|
|
39
|
+
await core_device_events.trigger_update()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@core_api.api_call(core_api.ACCESS_LEVEL_ADMIN)
|
|
43
|
+
async def patch_device(request: core_api.APIRequest, params: Attributes) -> None:
|
|
44
|
+
def unexpected_field_code(field: str) -> str:
|
|
45
|
+
if field in core_device_attrs.get_attrdefs():
|
|
46
|
+
return "attribute-not-modifiable"
|
|
47
|
+
else:
|
|
48
|
+
return "no-such-attribute"
|
|
49
|
+
|
|
50
|
+
core_api_schema.validate(
|
|
51
|
+
params,
|
|
52
|
+
core_device_attrs.get_schema(),
|
|
53
|
+
unexpected_field_code=unexpected_field_code,
|
|
54
|
+
unexpected_field_name="attribute",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
reboot_required = await core_device_attrs.set_attrs(params)
|
|
59
|
+
except core_device_attrs.DeviceAttributeError as e:
|
|
60
|
+
raise core_api.APIError(400, e.error, attribute=e.attribute)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
raise core_api.APIError(500, "unexpected-error", message=str(e)) from e
|
|
63
|
+
|
|
64
|
+
await core_device.save()
|
|
65
|
+
await core_device_events.trigger_update()
|
|
66
|
+
|
|
67
|
+
if reboot_required:
|
|
68
|
+
main.loop.call_later(2, system.reboot)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from qtoggleserver.core import api as core_api
|
|
4
|
+
from qtoggleserver.core.api import schema as core_api_schema
|
|
5
|
+
from qtoggleserver.core.typing import GenericJSONDict
|
|
6
|
+
from qtoggleserver.system import fwupdate
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@core_api.api_call(core_api.ACCESS_LEVEL_ADMIN)
|
|
13
|
+
async def get_firmware(request: core_api.APIRequest) -> GenericJSONDict:
|
|
14
|
+
current_version = await fwupdate.get_current_version()
|
|
15
|
+
status = await fwupdate.get_status()
|
|
16
|
+
|
|
17
|
+
if status in (fwupdate.STATUS_IDLE, fwupdate.STATUS_ERROR):
|
|
18
|
+
try:
|
|
19
|
+
latest_version, latest_date, latest_url = await fwupdate.get_latest()
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
"version": current_version,
|
|
23
|
+
"latest_version": latest_version,
|
|
24
|
+
"latest_date": latest_date,
|
|
25
|
+
"latest_url": latest_url,
|
|
26
|
+
"status": status,
|
|
27
|
+
}
|
|
28
|
+
except Exception as e:
|
|
29
|
+
logger.error("get latest firmware failed: %s", e, exc_info=True)
|
|
30
|
+
|
|
31
|
+
return {"version": current_version, "status": status}
|
|
32
|
+
else:
|
|
33
|
+
return {"version": current_version, "status": status}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@core_api.api_call(core_api.ACCESS_LEVEL_ADMIN)
|
|
37
|
+
async def patch_firmware(request: core_api.APIRequest, params: GenericJSONDict) -> None:
|
|
38
|
+
core_api_schema.validate(params, core_api_schema.PATCH_FIRMWARE)
|
|
39
|
+
|
|
40
|
+
status = await fwupdate.get_status()
|
|
41
|
+
if status not in (fwupdate.STATUS_IDLE, fwupdate.STATUS_ERROR):
|
|
42
|
+
raise core_api.APIError(503, "busy")
|
|
43
|
+
|
|
44
|
+
if params.get("url"):
|
|
45
|
+
await fwupdate.update_to_url(params["url"])
|
|
46
|
+
else: # assuming params['version']
|
|
47
|
+
await fwupdate.update_to_version(params["version"])
|