keepup-http 0.5.0__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.
- keepup_http/__init__.py +38 -0
- keepup_http/api_docs.py +76 -0
- keepup_http/body_limit.py +156 -0
- keepup_http/events_api.py +237 -0
- keepup_http/factory.py +562 -0
- keepup_http/lock_routes.py +155 -0
- keepup_http/plugin.py +53 -0
- keepup_http/routes.py +549 -0
- keepup_http/static/css/aos.css +1 -0
- keepup_http/static/css/main_nebula.css +232 -0
- keepup_http/static/css/main_new.css +869 -0
- keepup_http/static/css/tailwind.css +1 -0
- keepup_http/static/index_nebula.html +282 -0
- keepup_http/static/index_new.html +275 -0
- keepup_http/static/js/aos.js +1 -0
- keepup_http/static/js/feather-icons.js +13 -0
- keepup_http/static/js/layout_bootstrap.js +29 -0
- keepup_http/static/js/main_new.js +2076 -0
- keepup_http/static/js/tailwind.js +83 -0
- keepup_http/static/modules/css/background_tasks.css +233 -0
- keepup_http/static/modules/css/cluster.css +16 -0
- keepup_http/static/modules/css/event_manager.css +386 -0
- keepup_http/static/modules/css/integration_logs.css +33 -0
- keepup_http/static/modules/css/metrics.css +115 -0
- keepup_http/static/modules/css/modules.css +189 -0
- keepup_http/static/modules/css/themes.css +563 -0
- keepup_http/static/modules/css/users.css +278 -0
- keepup_http/static/modules/js/background_tasks.js +695 -0
- keepup_http/static/modules/js/chart.js +14 -0
- keepup_http/static/modules/js/chartjs-adapter-date-fns.bundle.min.js +7 -0
- keepup_http/static/modules/js/cluster.js +374 -0
- keepup_http/static/modules/js/event_manager.js +1007 -0
- keepup_http/static/modules/js/integration_logs.js +795 -0
- keepup_http/static/modules/js/metrics.js +929 -0
- keepup_http/static/modules/js/modules.js +1128 -0
- keepup_http/static/modules/js/themes.js +684 -0
- keepup_http/static/modules/js/users.js +898 -0
- keepup_http/web.py +214 -0
- keepup_http-0.5.0.dist-info/METADATA +29 -0
- keepup_http-0.5.0.dist-info/RECORD +43 -0
- keepup_http-0.5.0.dist-info/WHEEL +5 -0
- keepup_http-0.5.0.dist-info/entry_points.txt +2 -0
- keepup_http-0.5.0.dist-info/top_level.txt +1 -0
keepup_http/routes.py
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
"""How a declared route becomes an endpoint of the application.
|
|
2
|
+
|
|
3
|
+
A plugin's routes are data -- a path, a set of methods, a handler and a few
|
|
4
|
+
flags -- and this module is the single place where that data turns into
|
|
5
|
+
something FastAPI will call. The wrapper it builds signs the caller in, unpacks
|
|
6
|
+
the path and the query into the handler's arguments, parses a body when the
|
|
7
|
+
method carries one, and writes the call to the incoming request audit.
|
|
8
|
+
|
|
9
|
+
Single is the point. There used to be eight nearly identical wrappers here,
|
|
10
|
+
one per shape of route, and every change had to be made in all of them: the
|
|
11
|
+
request mask added a hundred and fifty lines by being added eight times, and
|
|
12
|
+
two of the eight had drifted apart from the rest without anybody noticing
|
|
13
|
+
(keepup-21). Every route of every plugin of every application is born here, so
|
|
14
|
+
what this module does once, it does to all of them.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
|
|
20
|
+
from fastapi import Depends, HTTPException, Request, Response
|
|
21
|
+
from starlette.requests import ClientDisconnect
|
|
22
|
+
|
|
23
|
+
from keepup_auth.websocket import authenticate_websocket
|
|
24
|
+
from keepup_http.body_limit import declare_limit
|
|
25
|
+
from keepup.kernel import observability, security
|
|
26
|
+
from keepup.kernel.call import (
|
|
27
|
+
KIND_WRITE,
|
|
28
|
+
Call,
|
|
29
|
+
CallError,
|
|
30
|
+
RouteSpec,
|
|
31
|
+
admit,
|
|
32
|
+
admitted,
|
|
33
|
+
invoke as invoke_route,
|
|
34
|
+
route_kind,
|
|
35
|
+
)
|
|
36
|
+
from keepup.plugins import route_mask
|
|
37
|
+
|
|
38
|
+
#: What an application may import from this module. Everything else is
|
|
39
|
+
#: internal and may change without notice -- see doc/keepup.md.
|
|
40
|
+
__all__ = [
|
|
41
|
+
"accepted_params",
|
|
42
|
+
"raw_request_wrapper",
|
|
43
|
+
"register_plugin_routes",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
#: The media type whose body the runtime parses. One type, compared as a media
|
|
49
|
+
#: type and not as a whole header -- see media_type().
|
|
50
|
+
JSON_MEDIA_TYPE = "application/json"
|
|
51
|
+
|
|
52
|
+
#: The structured syntax suffix of types that are JSON by construction
|
|
53
|
+
#: (RFC 6839): application/merge-patch+json and friends. They are recognised in
|
|
54
|
+
#: order to be named in the log, not in order to be read -- a route declares
|
|
55
|
+
#: what it accepts.
|
|
56
|
+
JSON_SUFFIX = "+json"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def current_subject(request: Request):
|
|
60
|
+
"""Who is calling, asked of whatever this deployment put behind ``auth``.
|
|
61
|
+
|
|
62
|
+
The kernel owns the question and this owns the HTTP half of it: a deployment
|
|
63
|
+
with no sign-in configured answers 401 rather than failing to import
|
|
64
|
+
something (keepup-119).
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
request: the request being served.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The caller, in whatever shape the deployment's identity system uses.
|
|
71
|
+
|
|
72
|
+
Raises:
|
|
73
|
+
HTTPException: 401, when this deployment has no identity system at all.
|
|
74
|
+
"""
|
|
75
|
+
dependency = security.subject_dependency()
|
|
76
|
+
if dependency is None:
|
|
77
|
+
raise HTTPException(status_code=401, detail="sign-in is not configured")
|
|
78
|
+
return await dependency(request)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def raw_request_wrapper(handler):
|
|
82
|
+
"""A route whose handler takes the request as it is and answers as it likes.
|
|
83
|
+
|
|
84
|
+
For surfaces with a wire format of their own -- the OpenAI-compatible gateway
|
|
85
|
+
answers errors in OpenAI's shape and streams server-sent events. The handler
|
|
86
|
+
authenticates the caller itself, and the body is not written to the incoming
|
|
87
|
+
request log: it carries users' prompts.
|
|
88
|
+
"""
|
|
89
|
+
async def wrapper_raw(request: Request):
|
|
90
|
+
return await handler(request=request)
|
|
91
|
+
|
|
92
|
+
return wrapper_raw
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def accepted_params(handler, params):
|
|
96
|
+
"""The parameters this handler declares, refusing the ones it does not.
|
|
97
|
+
|
|
98
|
+
The decision is the kernel's (:func:`keepup.kernel.call.admitted`); what an
|
|
99
|
+
HTTP caller reads is the refusal, which is a 400 here.
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
handler: the plugin's callable.
|
|
103
|
+
params: what arrived in the path and the query.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
The parameters to pass.
|
|
107
|
+
|
|
108
|
+
Raises:
|
|
109
|
+
HTTPException: 400, naming the parameters the route does not take.
|
|
110
|
+
"""
|
|
111
|
+
try:
|
|
112
|
+
return admitted(handler, params)
|
|
113
|
+
except CallError as refused:
|
|
114
|
+
raise HTTPException(status_code=refused.http_status, detail=refused.detail) from refused
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def admit_params(handler, mask, params):
|
|
118
|
+
"""What reaches the handler: the mask decides, or the signature still does.
|
|
119
|
+
|
|
120
|
+
A route that declared a mask is checked against it -- names, types,
|
|
121
|
+
required, allowed values -- and the values arrive converted. A route without
|
|
122
|
+
one keeps the previous behaviour exactly, which is the condition of the
|
|
123
|
+
change: the applications convert their routes by their own tasks, and a
|
|
124
|
+
framework that changed the default would have turned every route of every
|
|
125
|
+
application into a refusal at once. See doc/keepup.md.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
handler: the plugin's callable.
|
|
129
|
+
mask: the route's parsed mask, or None.
|
|
130
|
+
params: what arrived in the path and the query.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
The parameters to pass.
|
|
134
|
+
|
|
135
|
+
Raises:
|
|
136
|
+
HTTPException: 400, naming everything the request got wrong.
|
|
137
|
+
"""
|
|
138
|
+
spec = RouteSpec(path="", methods=(), handler=handler, mask=mask)
|
|
139
|
+
try:
|
|
140
|
+
return admit(spec, params)
|
|
141
|
+
except CallError as refused:
|
|
142
|
+
raise HTTPException(status_code=refused.http_status, detail=refused.detail) from refused
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def media_type(header):
|
|
146
|
+
"""The media type of a Content-Type header, without its parameters.
|
|
147
|
+
|
|
148
|
+
Lowercased and stripped of everything after the semicolon, because that is
|
|
149
|
+
what the standard says the type is: `application/json; charset=utf-8` and
|
|
150
|
+
`Application/JSON` name the same type as `application/json`. Comparing the
|
|
151
|
+
header as a whole string -- which is what this replaced -- lost the body of
|
|
152
|
+
every client that writes a charset, and most write one by default.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
header: the Content-Type header, or None when there is none.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
The media type in lower case, or an empty string when the header is
|
|
159
|
+
absent.
|
|
160
|
+
"""
|
|
161
|
+
return (header or "").split(";", 1)[0].strip().lower()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
async def json_body(request: Request, path: str = None):
|
|
165
|
+
"""The body of a write, parsed, or an empty one.
|
|
166
|
+
|
|
167
|
+
The content type decides, not the bytes: a handler written for a dictionary
|
|
168
|
+
would otherwise be handed a string the first time somebody posted a form.
|
|
169
|
+
A body that claims to be JSON and is not becomes empty rather than a
|
|
170
|
+
refusal -- the plugin is handed {} and decides for itself, because a
|
|
171
|
+
handler that needs a field says so by missing it.
|
|
172
|
+
|
|
173
|
+
That tolerance stays, but it is no longer silent. An empty body used to
|
|
174
|
+
mean three different things at once -- there was none, the type was not
|
|
175
|
+
JSON, or it arrived and could not be read -- and the handler, unable to
|
|
176
|
+
tell them apart, answered "field required" about a field the client had
|
|
177
|
+
sent. An agent writing its body in the machine's code page cost three rounds
|
|
178
|
+
of searching that way, with the word "encoding" in neither log. A body that
|
|
179
|
+
claims to be JSON and does not parse is now named in the log.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
request: the request being served.
|
|
183
|
+
path: the route's path as declared, for the log line. Defaults to the
|
|
184
|
+
address of this request, which names an instance rather than the
|
|
185
|
+
route.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
The parsed body, or {} when there is none, the type is not JSON, or it
|
|
189
|
+
could not be read.
|
|
190
|
+
"""
|
|
191
|
+
declared = media_type(request.headers.get("content-type"))
|
|
192
|
+
if declared != JSON_MEDIA_TYPE:
|
|
193
|
+
if declared.endswith(JSON_SUFFIX):
|
|
194
|
+
# JSON by construction (RFC 6839) and refused anyway: a route
|
|
195
|
+
# declares what it accepts, and a media type nobody declared must
|
|
196
|
+
# not start arriving because the runtime grew lenient. Named in the
|
|
197
|
+
# log, because the point of this whole function is that a body
|
|
198
|
+
# nobody reads does not vanish quietly.
|
|
199
|
+
logger.warning(
|
|
200
|
+
"body of a media type the runtime does not read: "
|
|
201
|
+
"route=%s method=%s content-type=%s",
|
|
202
|
+
path or request.url.path, request.method, declared)
|
|
203
|
+
return {}
|
|
204
|
+
body = None
|
|
205
|
+
try:
|
|
206
|
+
body = await request.body()
|
|
207
|
+
if not body:
|
|
208
|
+
# A write with no body at all is ordinary -- a great many routes
|
|
209
|
+
# take everything in the path. Warning about it would bury the
|
|
210
|
+
# lines this exists to produce.
|
|
211
|
+
return {}
|
|
212
|
+
return json.loads(body)
|
|
213
|
+
except ClientDisconnect:
|
|
214
|
+
# The body limit tells the application the client went away once a body
|
|
215
|
+
# without a declared length passes the route's limit. An empty body
|
|
216
|
+
# here would run the handler -- and its writes -- under a 413 the client
|
|
217
|
+
# was already answered with (keepup-61).
|
|
218
|
+
raise
|
|
219
|
+
except Exception as exc:
|
|
220
|
+
# Not a bare except, which used to be here: CancelledError does not
|
|
221
|
+
# inherit from Exception, and swallowing it turned a cancelled request
|
|
222
|
+
# into an empty body and a handler that ran on anyway.
|
|
223
|
+
#
|
|
224
|
+
# The body itself is never written: passwords and keys go through these
|
|
225
|
+
# routes. The length says one arrived without saying what was in it,
|
|
226
|
+
# and the exception text names a position and at most the single byte
|
|
227
|
+
# that could not be decoded -- which is what makes an encoding fault
|
|
228
|
+
# diagnosable at all. "unread" rather than 0 when reading itself
|
|
229
|
+
# failed: a client that disconnected mid-request sent a length nobody
|
|
230
|
+
# knows, and 0 would read as "sent nothing".
|
|
231
|
+
logger.warning(
|
|
232
|
+
"body declared as JSON could not be read: "
|
|
233
|
+
"route=%s method=%s content-type=%s length=%s reason=%s: %s",
|
|
234
|
+
path or request.url.path, request.method,
|
|
235
|
+
request.headers.get("content-type"),
|
|
236
|
+
len(body) if body is not None else "unread",
|
|
237
|
+
type(exc).__name__, exc)
|
|
238
|
+
return {}
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def create_wrapper(handler, path, methods, require_auth: bool = True,
|
|
242
|
+
is_upload: bool = False, mask=None, permission: str = None,
|
|
243
|
+
audit: bool = True, response_media_type: str = None):
|
|
244
|
+
"""Build the endpoint FastAPI will call for one declared route.
|
|
245
|
+
|
|
246
|
+
One implementation rather than one per shape. It used to be eight nearly
|
|
247
|
+
identical copies -- three shapes x a path parameter x authentication -- and
|
|
248
|
+
two of them had quietly drifted apart from the rest (keepup-21).
|
|
249
|
+
|
|
250
|
+
Two signatures remain, and only because FastAPI builds an endpoint out of
|
|
251
|
+
one: a route that is signed in has to declare the dependency, and a public
|
|
252
|
+
route must not.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
handler: the plugin's callable.
|
|
256
|
+
path: the route's path as declared, parameters and all.
|
|
257
|
+
methods: the methods the route declares.
|
|
258
|
+
require_auth: whether the framework signs the caller in.
|
|
259
|
+
is_upload: whether the handler is handed the request instead of a body.
|
|
260
|
+
mask: the route's parsed mask, or None.
|
|
261
|
+
permission: the right the caller must hold, or None
|
|
262
|
+
(keepup/auth/identity/access.py).
|
|
263
|
+
audit: whether the call is written to the incoming-request audit. A
|
|
264
|
+
route that answers a monitor is asked every few seconds by every
|
|
265
|
+
replica, and a row per scrape is not an audit (keepup-102).
|
|
266
|
+
response_media_type: what the answer is, when it is not JSON -- text
|
|
267
|
+
for a metrics scrape, for one. A handler that returns a Response
|
|
268
|
+
itself is passed through either way.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
An async endpoint to hand to app.add_api_route().
|
|
272
|
+
"""
|
|
273
|
+
kind, method_name = route_kind(methods)
|
|
274
|
+
if kind is None:
|
|
275
|
+
logger.warning(f"No matching wrapper found for {path} with methods {methods}")
|
|
276
|
+
|
|
277
|
+
# It takes nothing on purpose. Written as (*args, **kwargs) it never
|
|
278
|
+
# answered 501 at all: FastAPI reads a signature to build the endpoint,
|
|
279
|
+
# so those two became required query parameters and the route answered
|
|
280
|
+
# 422 "args, kwargs: field required" (keepup-20).
|
|
281
|
+
async def fallback_wrapper():
|
|
282
|
+
raise HTTPException(status_code=501,
|
|
283
|
+
detail=f"Not implemented for methods {methods}")
|
|
284
|
+
|
|
285
|
+
return fallback_wrapper
|
|
286
|
+
|
|
287
|
+
has_path_params = any('{' in part and '}' in part for part in path.split('/'))
|
|
288
|
+
|
|
289
|
+
def as_answer(value):
|
|
290
|
+
"""A handler's value as the answer: a Response as it is, text when asked."""
|
|
291
|
+
if response_media_type is None or isinstance(value, Response):
|
|
292
|
+
return value
|
|
293
|
+
return Response(content="" if value is None else str(value),
|
|
294
|
+
media_type=response_media_type)
|
|
295
|
+
|
|
296
|
+
spec = RouteSpec(
|
|
297
|
+
path=path,
|
|
298
|
+
methods=tuple(str(item).upper() for item in methods or ()),
|
|
299
|
+
handler=handler,
|
|
300
|
+
mask=mask,
|
|
301
|
+
permission=permission,
|
|
302
|
+
require_auth=require_auth,
|
|
303
|
+
is_upload=is_upload,
|
|
304
|
+
audit=audit,
|
|
305
|
+
response_media_type=response_media_type,
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
async def invoke(request: Request, current_user):
|
|
309
|
+
"""What the handler does, with or without the audit around it.
|
|
310
|
+
|
|
311
|
+
The reading of the request is the HTTP adapter's; everything after it --
|
|
312
|
+
the mask, the caller, the body, the right, the answer -- is the
|
|
313
|
+
transport-neutral invocation of ``keepup.kernel.call``.
|
|
314
|
+
"""
|
|
315
|
+
params = {}
|
|
316
|
+
# The query first and the path over it: a value taken from the address
|
|
317
|
+
# is what the route matched on, and a client that sends ?id=99 to
|
|
318
|
+
# /api/x/5 used to reach the handler with 99 while every log and every
|
|
319
|
+
# rule built on the path still said 5.
|
|
320
|
+
params.update(request.query_params)
|
|
321
|
+
params.update(request.path_params)
|
|
322
|
+
|
|
323
|
+
body = None
|
|
324
|
+
if kind == KIND_WRITE:
|
|
325
|
+
# An upload is handed the request itself: reading a file into
|
|
326
|
+
# memory to pass it as a value is what such a route exists to avoid.
|
|
327
|
+
body = request if is_upload else await json_body(request, path)
|
|
328
|
+
|
|
329
|
+
call = Call(route=spec, params=params, actor=current_user, body=body, source="http")
|
|
330
|
+
try:
|
|
331
|
+
answer = await invoke_route(call, checker=security.checker())
|
|
332
|
+
except CallError as refused:
|
|
333
|
+
raise HTTPException(status_code=refused.http_status, detail=refused.detail) from refused
|
|
334
|
+
return as_answer(answer)
|
|
335
|
+
|
|
336
|
+
async def call(request: Request, current_user):
|
|
337
|
+
"""One call of a plugin route, from the audit's first row to its last."""
|
|
338
|
+
request_data = {
|
|
339
|
+
'user_id': current_user.get('id') if current_user else None,
|
|
340
|
+
'username': current_user.get('username') if current_user else 'anonymous',
|
|
341
|
+
}
|
|
342
|
+
if has_path_params:
|
|
343
|
+
request_data['path_params'] = dict(request.path_params)
|
|
344
|
+
request_data['query_params'] = dict(request.query_params)
|
|
345
|
+
if kind == KIND_WRITE:
|
|
346
|
+
request_data['is_upload'] = is_upload
|
|
347
|
+
|
|
348
|
+
if not audit:
|
|
349
|
+
# A route that said so is not written down: no context, no row.
|
|
350
|
+
return await invoke(request, current_user)
|
|
351
|
+
|
|
352
|
+
async with observability.start_call(
|
|
353
|
+
method=method_name,
|
|
354
|
+
endpoint=path,
|
|
355
|
+
host='plugin_api',
|
|
356
|
+
request_data=request_data
|
|
357
|
+
) as request_id:
|
|
358
|
+
try:
|
|
359
|
+
response = await invoke(request, current_user)
|
|
360
|
+
|
|
361
|
+
await observability.end_call(
|
|
362
|
+
request_id=request_id,
|
|
363
|
+
http_status=200,
|
|
364
|
+
response_data=response if isinstance(response, dict) else {"data": response}
|
|
365
|
+
)
|
|
366
|
+
|
|
367
|
+
return response
|
|
368
|
+
except HTTPException as e:
|
|
369
|
+
await observability.end_call(
|
|
370
|
+
request_id=request_id,
|
|
371
|
+
http_status=e.status_code,
|
|
372
|
+
error_message=e.detail
|
|
373
|
+
)
|
|
374
|
+
raise
|
|
375
|
+
except Exception as e:
|
|
376
|
+
# The kind of failure, not its text: the text of an unexpected
|
|
377
|
+
# exception carries whatever the failing code held -- values,
|
|
378
|
+
# queries, secrets -- and the audit's redaction never sees it
|
|
379
|
+
# (keepup-76). The text goes to the application log.
|
|
380
|
+
await observability.end_call(
|
|
381
|
+
request_id=request_id,
|
|
382
|
+
http_status=500,
|
|
383
|
+
error_message=type(e).__name__
|
|
384
|
+
)
|
|
385
|
+
raise
|
|
386
|
+
|
|
387
|
+
if require_auth:
|
|
388
|
+
# The dependency FastAPI resolves, chosen when the route is registered:
|
|
389
|
+
# whatever the deployment put behind `auth` -- the framework's own
|
|
390
|
+
# sign-in until keepup-auth is a plugin -- or this module's refusal, so
|
|
391
|
+
# that a process with no identity system answers 401 instead of failing
|
|
392
|
+
# to import one (keepup-119). A dependency, not a call: the registered
|
|
393
|
+
# one has sub-dependencies of its own, and FastAPI is what resolves
|
|
394
|
+
# them.
|
|
395
|
+
dependency = security.subject_dependency() or current_subject
|
|
396
|
+
|
|
397
|
+
async def wrapper_signed_in(request: Request,
|
|
398
|
+
current_user: dict = Depends(dependency)):
|
|
399
|
+
return await call(request, current_user)
|
|
400
|
+
|
|
401
|
+
return wrapper_signed_in
|
|
402
|
+
|
|
403
|
+
async def wrapper_public(request: Request):
|
|
404
|
+
return await call(request, None)
|
|
405
|
+
|
|
406
|
+
return wrapper_public
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
async def register_plugin_routes(app, manager):
|
|
410
|
+
"""Register every plugin's API routes on the FastAPI application."""
|
|
411
|
+
|
|
412
|
+
routes = manager.get_all_api_routes()
|
|
413
|
+
logger.info(f"Found {len(routes)} plugin routes to register")
|
|
414
|
+
|
|
415
|
+
for route in routes:
|
|
416
|
+
handler = route['handler']
|
|
417
|
+
path = route['path']
|
|
418
|
+
methods = route['methods']
|
|
419
|
+
require_auth = route.get('require_auth', True)
|
|
420
|
+
is_upload = route.get('is_upload', False)
|
|
421
|
+
permission = route.get('permission')
|
|
422
|
+
audit = route.get('audit', True)
|
|
423
|
+
response_media_type = route.get('response_media_type')
|
|
424
|
+
if response_media_type is not None and (
|
|
425
|
+
not isinstance(response_media_type, str) or not response_media_type.strip()):
|
|
426
|
+
raise ValueError(
|
|
427
|
+
f"{path}: response_media_type is a media type, like 'text/plain'")
|
|
428
|
+
if response_media_type is not None and route.get('raw_request'):
|
|
429
|
+
raise ValueError(
|
|
430
|
+
f"{path}: a raw_request route answers with a Response of its own; "
|
|
431
|
+
"response_media_type would be ignored. Drop one of the two.")
|
|
432
|
+
if permission is not None:
|
|
433
|
+
# A right needs somebody to hold it: on a route nobody signs in to,
|
|
434
|
+
# or one that signs itself in, it would be a check that never runs.
|
|
435
|
+
if not isinstance(permission, str) or not permission.strip():
|
|
436
|
+
raise ValueError(f"{path}: permission must be a non-empty string")
|
|
437
|
+
if route.get('raw_request') or not require_auth:
|
|
438
|
+
raise ValueError(
|
|
439
|
+
f"{path}: a route that declares a permission must be signed in by "
|
|
440
|
+
"the framework -- drop require_auth: false or raw_request, or the "
|
|
441
|
+
"permission")
|
|
442
|
+
|
|
443
|
+
logger.info(
|
|
444
|
+
f"Registering route {path} - methods: {methods}, auth: {require_auth}, upload: {is_upload}")
|
|
445
|
+
|
|
446
|
+
# The kernel parsed and checked the mask when it collected the route,
|
|
447
|
+
# for every transport there will ever be (keepup-102); a route that
|
|
448
|
+
# reaches here without one was declared by something else, and is
|
|
449
|
+
# checked the same way rather than trusted.
|
|
450
|
+
mask = route.get('_mask')
|
|
451
|
+
if mask is None:
|
|
452
|
+
try:
|
|
453
|
+
mask = route_mask.parse(route.get(route_mask.MASK_FIELD), path)
|
|
454
|
+
route_mask.check_signature(handler, mask)
|
|
455
|
+
except route_mask.MaskError as error:
|
|
456
|
+
# The same loudness as the raw_request check below: a mask naming
|
|
457
|
+
# a parameter the handler does not take is a typo, and finding it
|
|
458
|
+
# on a request would mean hearing about it from a user.
|
|
459
|
+
raise ValueError(f"{path}: {error}")
|
|
460
|
+
|
|
461
|
+
if route.get('raw_request'):
|
|
462
|
+
# A raw route authenticates inside the handler -- it is handed the
|
|
463
|
+
# Request and nothing else. Declaring require_auth on it therefore
|
|
464
|
+
# promises a check the framework does not make, and the start-up
|
|
465
|
+
# line used to print auth=True over an open route (keepup-12).
|
|
466
|
+
if route.get('require_auth') is True:
|
|
467
|
+
raise ValueError(
|
|
468
|
+
f"{path}: a raw_request route cannot also declare "
|
|
469
|
+
"require_auth -- the handler receives the Request and "
|
|
470
|
+
"authenticates itself. Drop one of the two.")
|
|
471
|
+
require_auth = False
|
|
472
|
+
wrapper = raw_request_wrapper(handler)
|
|
473
|
+
else:
|
|
474
|
+
wrapper = create_wrapper(handler, path, methods, require_auth, is_upload, mask,
|
|
475
|
+
permission, audit=audit,
|
|
476
|
+
response_media_type=response_media_type)
|
|
477
|
+
|
|
478
|
+
# What the route says about its body: its own limit, or that it reads
|
|
479
|
+
# the body itself (keepup/body_limit.py).
|
|
480
|
+
declare_limit(wrapper, route)
|
|
481
|
+
|
|
482
|
+
app.add_api_route(
|
|
483
|
+
path,
|
|
484
|
+
wrapper,
|
|
485
|
+
methods=methods,
|
|
486
|
+
include_in_schema=route.get('include_in_schema', True)
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
logger.info(
|
|
490
|
+
f"Registered plugin route: {path} - {methods} (auth={require_auth}, upload={is_upload})")
|
|
491
|
+
|
|
492
|
+
websocket_routes = manager.get_all_websocket_routes()
|
|
493
|
+
logger.info(f"Found {len(websocket_routes)} WebSocket plugin routes to register")
|
|
494
|
+
|
|
495
|
+
for ws_route in websocket_routes:
|
|
496
|
+
path = ws_route['path']
|
|
497
|
+
handler = ws_route['handler']
|
|
498
|
+
# Off by default, unlike HTTP routes: sockets of agents and guests carry
|
|
499
|
+
# tokens of their own and would be refused by the panel's sign-in rule.
|
|
500
|
+
if ws_route.get('require_auth', False):
|
|
501
|
+
handler = signed_in_websocket(handler)
|
|
502
|
+
_register_websocket(app, path, handler)
|
|
503
|
+
logger.info(f"Registered WebSocket plugin route: {path} "
|
|
504
|
+
f"(auth={bool(ws_route.get('require_auth', False))})")
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
def _register_websocket(app, path, handler):
|
|
508
|
+
"""Register a socket route on the application, whichever Starlette it has.
|
|
509
|
+
|
|
510
|
+
The route is appended as a plain ``WebSocketRoute`` rather than through the
|
|
511
|
+
application's helpers, because neither helper works on both lines:
|
|
512
|
+
Starlette 1.0 dropped ``add_websocket_route``, and FastAPI's
|
|
513
|
+
``add_api_websocket_route`` reads the handler's signature and refuses one
|
|
514
|
+
whose socket parameter carries no annotation -- which a plugin's handler
|
|
515
|
+
need not carry.
|
|
516
|
+
|
|
517
|
+
It is done this way rather than by pinning Starlette below 1.0, because
|
|
518
|
+
that pin is what held installations on a version with four published
|
|
519
|
+
advisories (keepup-30).
|
|
520
|
+
|
|
521
|
+
Args:
|
|
522
|
+
app: The FastAPI application.
|
|
523
|
+
path: The address the socket answers on.
|
|
524
|
+
handler: The coroutine serving the connection.
|
|
525
|
+
"""
|
|
526
|
+
from starlette.routing import WebSocketRoute
|
|
527
|
+
|
|
528
|
+
app.router.routes.append(WebSocketRoute(path, handler))
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def signed_in_websocket(handler):
|
|
532
|
+
"""Wrap a socket handler so it runs only for a signed-in user, who is passed in.
|
|
533
|
+
|
|
534
|
+
Refusal closes the socket with 1008 before the handler sees it.
|
|
535
|
+
"""
|
|
536
|
+
async def wrapper(websocket):
|
|
537
|
+
user = await authenticate_websocket(websocket)
|
|
538
|
+
if user is None:
|
|
539
|
+
return
|
|
540
|
+
await handler(websocket, current_user=user)
|
|
541
|
+
|
|
542
|
+
return wrapper
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
# --- Post-construct and the administrative surface ----------------------------
|
|
546
|
+
#
|
|
547
|
+
# post_construct runs after the server is up, for the self-checks that need a
|
|
548
|
+
# live HTTP server; the endpoints below are what an administrator sees of the
|
|
549
|
+
# runtime -- which plugins are declared, which are running, and why one is not.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
[data-aos][data-aos][data-aos-duration="50"],body[data-aos-duration="50"] [data-aos]{transition-duration:50ms}[data-aos][data-aos][data-aos-delay="50"],body[data-aos-delay="50"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="50"].aos-animate,body[data-aos-delay="50"] [data-aos].aos-animate{transition-delay:50ms}[data-aos][data-aos][data-aos-duration="100"],body[data-aos-duration="100"] [data-aos]{transition-duration:.1s}[data-aos][data-aos][data-aos-delay="100"],body[data-aos-delay="100"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="100"].aos-animate,body[data-aos-delay="100"] [data-aos].aos-animate{transition-delay:.1s}[data-aos][data-aos][data-aos-duration="150"],body[data-aos-duration="150"] [data-aos]{transition-duration:.15s}[data-aos][data-aos][data-aos-delay="150"],body[data-aos-delay="150"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="150"].aos-animate,body[data-aos-delay="150"] [data-aos].aos-animate{transition-delay:.15s}[data-aos][data-aos][data-aos-duration="200"],body[data-aos-duration="200"] [data-aos]{transition-duration:.2s}[data-aos][data-aos][data-aos-delay="200"],body[data-aos-delay="200"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="200"].aos-animate,body[data-aos-delay="200"] [data-aos].aos-animate{transition-delay:.2s}[data-aos][data-aos][data-aos-duration="250"],body[data-aos-duration="250"] [data-aos]{transition-duration:.25s}[data-aos][data-aos][data-aos-delay="250"],body[data-aos-delay="250"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="250"].aos-animate,body[data-aos-delay="250"] [data-aos].aos-animate{transition-delay:.25s}[data-aos][data-aos][data-aos-duration="300"],body[data-aos-duration="300"] [data-aos]{transition-duration:.3s}[data-aos][data-aos][data-aos-delay="300"],body[data-aos-delay="300"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="300"].aos-animate,body[data-aos-delay="300"] [data-aos].aos-animate{transition-delay:.3s}[data-aos][data-aos][data-aos-duration="350"],body[data-aos-duration="350"] [data-aos]{transition-duration:.35s}[data-aos][data-aos][data-aos-delay="350"],body[data-aos-delay="350"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="350"].aos-animate,body[data-aos-delay="350"] [data-aos].aos-animate{transition-delay:.35s}[data-aos][data-aos][data-aos-duration="400"],body[data-aos-duration="400"] [data-aos]{transition-duration:.4s}[data-aos][data-aos][data-aos-delay="400"],body[data-aos-delay="400"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="400"].aos-animate,body[data-aos-delay="400"] [data-aos].aos-animate{transition-delay:.4s}[data-aos][data-aos][data-aos-duration="450"],body[data-aos-duration="450"] [data-aos]{transition-duration:.45s}[data-aos][data-aos][data-aos-delay="450"],body[data-aos-delay="450"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="450"].aos-animate,body[data-aos-delay="450"] [data-aos].aos-animate{transition-delay:.45s}[data-aos][data-aos][data-aos-duration="500"],body[data-aos-duration="500"] [data-aos]{transition-duration:.5s}[data-aos][data-aos][data-aos-delay="500"],body[data-aos-delay="500"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="500"].aos-animate,body[data-aos-delay="500"] [data-aos].aos-animate{transition-delay:.5s}[data-aos][data-aos][data-aos-duration="550"],body[data-aos-duration="550"] [data-aos]{transition-duration:.55s}[data-aos][data-aos][data-aos-delay="550"],body[data-aos-delay="550"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="550"].aos-animate,body[data-aos-delay="550"] [data-aos].aos-animate{transition-delay:.55s}[data-aos][data-aos][data-aos-duration="600"],body[data-aos-duration="600"] [data-aos]{transition-duration:.6s}[data-aos][data-aos][data-aos-delay="600"],body[data-aos-delay="600"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="600"].aos-animate,body[data-aos-delay="600"] [data-aos].aos-animate{transition-delay:.6s}[data-aos][data-aos][data-aos-duration="650"],body[data-aos-duration="650"] [data-aos]{transition-duration:.65s}[data-aos][data-aos][data-aos-delay="650"],body[data-aos-delay="650"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="650"].aos-animate,body[data-aos-delay="650"] [data-aos].aos-animate{transition-delay:.65s}[data-aos][data-aos][data-aos-duration="700"],body[data-aos-duration="700"] [data-aos]{transition-duration:.7s}[data-aos][data-aos][data-aos-delay="700"],body[data-aos-delay="700"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="700"].aos-animate,body[data-aos-delay="700"] [data-aos].aos-animate{transition-delay:.7s}[data-aos][data-aos][data-aos-duration="750"],body[data-aos-duration="750"] [data-aos]{transition-duration:.75s}[data-aos][data-aos][data-aos-delay="750"],body[data-aos-delay="750"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="750"].aos-animate,body[data-aos-delay="750"] [data-aos].aos-animate{transition-delay:.75s}[data-aos][data-aos][data-aos-duration="800"],body[data-aos-duration="800"] [data-aos]{transition-duration:.8s}[data-aos][data-aos][data-aos-delay="800"],body[data-aos-delay="800"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="800"].aos-animate,body[data-aos-delay="800"] [data-aos].aos-animate{transition-delay:.8s}[data-aos][data-aos][data-aos-duration="850"],body[data-aos-duration="850"] [data-aos]{transition-duration:.85s}[data-aos][data-aos][data-aos-delay="850"],body[data-aos-delay="850"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="850"].aos-animate,body[data-aos-delay="850"] [data-aos].aos-animate{transition-delay:.85s}[data-aos][data-aos][data-aos-duration="900"],body[data-aos-duration="900"] [data-aos]{transition-duration:.9s}[data-aos][data-aos][data-aos-delay="900"],body[data-aos-delay="900"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="900"].aos-animate,body[data-aos-delay="900"] [data-aos].aos-animate{transition-delay:.9s}[data-aos][data-aos][data-aos-duration="950"],body[data-aos-duration="950"] [data-aos]{transition-duration:.95s}[data-aos][data-aos][data-aos-delay="950"],body[data-aos-delay="950"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="950"].aos-animate,body[data-aos-delay="950"] [data-aos].aos-animate{transition-delay:.95s}[data-aos][data-aos][data-aos-duration="1000"],body[data-aos-duration="1000"] [data-aos]{transition-duration:1s}[data-aos][data-aos][data-aos-delay="1000"],body[data-aos-delay="1000"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1000"].aos-animate,body[data-aos-delay="1000"] [data-aos].aos-animate{transition-delay:1s}[data-aos][data-aos][data-aos-duration="1050"],body[data-aos-duration="1050"] [data-aos]{transition-duration:1.05s}[data-aos][data-aos][data-aos-delay="1050"],body[data-aos-delay="1050"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1050"].aos-animate,body[data-aos-delay="1050"] [data-aos].aos-animate{transition-delay:1.05s}[data-aos][data-aos][data-aos-duration="1100"],body[data-aos-duration="1100"] [data-aos]{transition-duration:1.1s}[data-aos][data-aos][data-aos-delay="1100"],body[data-aos-delay="1100"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1100"].aos-animate,body[data-aos-delay="1100"] [data-aos].aos-animate{transition-delay:1.1s}[data-aos][data-aos][data-aos-duration="1150"],body[data-aos-duration="1150"] [data-aos]{transition-duration:1.15s}[data-aos][data-aos][data-aos-delay="1150"],body[data-aos-delay="1150"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1150"].aos-animate,body[data-aos-delay="1150"] [data-aos].aos-animate{transition-delay:1.15s}[data-aos][data-aos][data-aos-duration="1200"],body[data-aos-duration="1200"] [data-aos]{transition-duration:1.2s}[data-aos][data-aos][data-aos-delay="1200"],body[data-aos-delay="1200"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1200"].aos-animate,body[data-aos-delay="1200"] [data-aos].aos-animate{transition-delay:1.2s}[data-aos][data-aos][data-aos-duration="1250"],body[data-aos-duration="1250"] [data-aos]{transition-duration:1.25s}[data-aos][data-aos][data-aos-delay="1250"],body[data-aos-delay="1250"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1250"].aos-animate,body[data-aos-delay="1250"] [data-aos].aos-animate{transition-delay:1.25s}[data-aos][data-aos][data-aos-duration="1300"],body[data-aos-duration="1300"] [data-aos]{transition-duration:1.3s}[data-aos][data-aos][data-aos-delay="1300"],body[data-aos-delay="1300"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1300"].aos-animate,body[data-aos-delay="1300"] [data-aos].aos-animate{transition-delay:1.3s}[data-aos][data-aos][data-aos-duration="1350"],body[data-aos-duration="1350"] [data-aos]{transition-duration:1.35s}[data-aos][data-aos][data-aos-delay="1350"],body[data-aos-delay="1350"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1350"].aos-animate,body[data-aos-delay="1350"] [data-aos].aos-animate{transition-delay:1.35s}[data-aos][data-aos][data-aos-duration="1400"],body[data-aos-duration="1400"] [data-aos]{transition-duration:1.4s}[data-aos][data-aos][data-aos-delay="1400"],body[data-aos-delay="1400"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1400"].aos-animate,body[data-aos-delay="1400"] [data-aos].aos-animate{transition-delay:1.4s}[data-aos][data-aos][data-aos-duration="1450"],body[data-aos-duration="1450"] [data-aos]{transition-duration:1.45s}[data-aos][data-aos][data-aos-delay="1450"],body[data-aos-delay="1450"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1450"].aos-animate,body[data-aos-delay="1450"] [data-aos].aos-animate{transition-delay:1.45s}[data-aos][data-aos][data-aos-duration="1500"],body[data-aos-duration="1500"] [data-aos]{transition-duration:1.5s}[data-aos][data-aos][data-aos-delay="1500"],body[data-aos-delay="1500"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1500"].aos-animate,body[data-aos-delay="1500"] [data-aos].aos-animate{transition-delay:1.5s}[data-aos][data-aos][data-aos-duration="1550"],body[data-aos-duration="1550"] [data-aos]{transition-duration:1.55s}[data-aos][data-aos][data-aos-delay="1550"],body[data-aos-delay="1550"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1550"].aos-animate,body[data-aos-delay="1550"] [data-aos].aos-animate{transition-delay:1.55s}[data-aos][data-aos][data-aos-duration="1600"],body[data-aos-duration="1600"] [data-aos]{transition-duration:1.6s}[data-aos][data-aos][data-aos-delay="1600"],body[data-aos-delay="1600"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1600"].aos-animate,body[data-aos-delay="1600"] [data-aos].aos-animate{transition-delay:1.6s}[data-aos][data-aos][data-aos-duration="1650"],body[data-aos-duration="1650"] [data-aos]{transition-duration:1.65s}[data-aos][data-aos][data-aos-delay="1650"],body[data-aos-delay="1650"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1650"].aos-animate,body[data-aos-delay="1650"] [data-aos].aos-animate{transition-delay:1.65s}[data-aos][data-aos][data-aos-duration="1700"],body[data-aos-duration="1700"] [data-aos]{transition-duration:1.7s}[data-aos][data-aos][data-aos-delay="1700"],body[data-aos-delay="1700"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1700"].aos-animate,body[data-aos-delay="1700"] [data-aos].aos-animate{transition-delay:1.7s}[data-aos][data-aos][data-aos-duration="1750"],body[data-aos-duration="1750"] [data-aos]{transition-duration:1.75s}[data-aos][data-aos][data-aos-delay="1750"],body[data-aos-delay="1750"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1750"].aos-animate,body[data-aos-delay="1750"] [data-aos].aos-animate{transition-delay:1.75s}[data-aos][data-aos][data-aos-duration="1800"],body[data-aos-duration="1800"] [data-aos]{transition-duration:1.8s}[data-aos][data-aos][data-aos-delay="1800"],body[data-aos-delay="1800"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1800"].aos-animate,body[data-aos-delay="1800"] [data-aos].aos-animate{transition-delay:1.8s}[data-aos][data-aos][data-aos-duration="1850"],body[data-aos-duration="1850"] [data-aos]{transition-duration:1.85s}[data-aos][data-aos][data-aos-delay="1850"],body[data-aos-delay="1850"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1850"].aos-animate,body[data-aos-delay="1850"] [data-aos].aos-animate{transition-delay:1.85s}[data-aos][data-aos][data-aos-duration="1900"],body[data-aos-duration="1900"] [data-aos]{transition-duration:1.9s}[data-aos][data-aos][data-aos-delay="1900"],body[data-aos-delay="1900"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1900"].aos-animate,body[data-aos-delay="1900"] [data-aos].aos-animate{transition-delay:1.9s}[data-aos][data-aos][data-aos-duration="1950"],body[data-aos-duration="1950"] [data-aos]{transition-duration:1.95s}[data-aos][data-aos][data-aos-delay="1950"],body[data-aos-delay="1950"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="1950"].aos-animate,body[data-aos-delay="1950"] [data-aos].aos-animate{transition-delay:1.95s}[data-aos][data-aos][data-aos-duration="2000"],body[data-aos-duration="2000"] [data-aos]{transition-duration:2s}[data-aos][data-aos][data-aos-delay="2000"],body[data-aos-delay="2000"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2000"].aos-animate,body[data-aos-delay="2000"] [data-aos].aos-animate{transition-delay:2s}[data-aos][data-aos][data-aos-duration="2050"],body[data-aos-duration="2050"] [data-aos]{transition-duration:2.05s}[data-aos][data-aos][data-aos-delay="2050"],body[data-aos-delay="2050"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2050"].aos-animate,body[data-aos-delay="2050"] [data-aos].aos-animate{transition-delay:2.05s}[data-aos][data-aos][data-aos-duration="2100"],body[data-aos-duration="2100"] [data-aos]{transition-duration:2.1s}[data-aos][data-aos][data-aos-delay="2100"],body[data-aos-delay="2100"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2100"].aos-animate,body[data-aos-delay="2100"] [data-aos].aos-animate{transition-delay:2.1s}[data-aos][data-aos][data-aos-duration="2150"],body[data-aos-duration="2150"] [data-aos]{transition-duration:2.15s}[data-aos][data-aos][data-aos-delay="2150"],body[data-aos-delay="2150"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2150"].aos-animate,body[data-aos-delay="2150"] [data-aos].aos-animate{transition-delay:2.15s}[data-aos][data-aos][data-aos-duration="2200"],body[data-aos-duration="2200"] [data-aos]{transition-duration:2.2s}[data-aos][data-aos][data-aos-delay="2200"],body[data-aos-delay="2200"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2200"].aos-animate,body[data-aos-delay="2200"] [data-aos].aos-animate{transition-delay:2.2s}[data-aos][data-aos][data-aos-duration="2250"],body[data-aos-duration="2250"] [data-aos]{transition-duration:2.25s}[data-aos][data-aos][data-aos-delay="2250"],body[data-aos-delay="2250"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2250"].aos-animate,body[data-aos-delay="2250"] [data-aos].aos-animate{transition-delay:2.25s}[data-aos][data-aos][data-aos-duration="2300"],body[data-aos-duration="2300"] [data-aos]{transition-duration:2.3s}[data-aos][data-aos][data-aos-delay="2300"],body[data-aos-delay="2300"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2300"].aos-animate,body[data-aos-delay="2300"] [data-aos].aos-animate{transition-delay:2.3s}[data-aos][data-aos][data-aos-duration="2350"],body[data-aos-duration="2350"] [data-aos]{transition-duration:2.35s}[data-aos][data-aos][data-aos-delay="2350"],body[data-aos-delay="2350"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2350"].aos-animate,body[data-aos-delay="2350"] [data-aos].aos-animate{transition-delay:2.35s}[data-aos][data-aos][data-aos-duration="2400"],body[data-aos-duration="2400"] [data-aos]{transition-duration:2.4s}[data-aos][data-aos][data-aos-delay="2400"],body[data-aos-delay="2400"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2400"].aos-animate,body[data-aos-delay="2400"] [data-aos].aos-animate{transition-delay:2.4s}[data-aos][data-aos][data-aos-duration="2450"],body[data-aos-duration="2450"] [data-aos]{transition-duration:2.45s}[data-aos][data-aos][data-aos-delay="2450"],body[data-aos-delay="2450"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2450"].aos-animate,body[data-aos-delay="2450"] [data-aos].aos-animate{transition-delay:2.45s}[data-aos][data-aos][data-aos-duration="2500"],body[data-aos-duration="2500"] [data-aos]{transition-duration:2.5s}[data-aos][data-aos][data-aos-delay="2500"],body[data-aos-delay="2500"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2500"].aos-animate,body[data-aos-delay="2500"] [data-aos].aos-animate{transition-delay:2.5s}[data-aos][data-aos][data-aos-duration="2550"],body[data-aos-duration="2550"] [data-aos]{transition-duration:2.55s}[data-aos][data-aos][data-aos-delay="2550"],body[data-aos-delay="2550"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2550"].aos-animate,body[data-aos-delay="2550"] [data-aos].aos-animate{transition-delay:2.55s}[data-aos][data-aos][data-aos-duration="2600"],body[data-aos-duration="2600"] [data-aos]{transition-duration:2.6s}[data-aos][data-aos][data-aos-delay="2600"],body[data-aos-delay="2600"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2600"].aos-animate,body[data-aos-delay="2600"] [data-aos].aos-animate{transition-delay:2.6s}[data-aos][data-aos][data-aos-duration="2650"],body[data-aos-duration="2650"] [data-aos]{transition-duration:2.65s}[data-aos][data-aos][data-aos-delay="2650"],body[data-aos-delay="2650"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2650"].aos-animate,body[data-aos-delay="2650"] [data-aos].aos-animate{transition-delay:2.65s}[data-aos][data-aos][data-aos-duration="2700"],body[data-aos-duration="2700"] [data-aos]{transition-duration:2.7s}[data-aos][data-aos][data-aos-delay="2700"],body[data-aos-delay="2700"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2700"].aos-animate,body[data-aos-delay="2700"] [data-aos].aos-animate{transition-delay:2.7s}[data-aos][data-aos][data-aos-duration="2750"],body[data-aos-duration="2750"] [data-aos]{transition-duration:2.75s}[data-aos][data-aos][data-aos-delay="2750"],body[data-aos-delay="2750"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2750"].aos-animate,body[data-aos-delay="2750"] [data-aos].aos-animate{transition-delay:2.75s}[data-aos][data-aos][data-aos-duration="2800"],body[data-aos-duration="2800"] [data-aos]{transition-duration:2.8s}[data-aos][data-aos][data-aos-delay="2800"],body[data-aos-delay="2800"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2800"].aos-animate,body[data-aos-delay="2800"] [data-aos].aos-animate{transition-delay:2.8s}[data-aos][data-aos][data-aos-duration="2850"],body[data-aos-duration="2850"] [data-aos]{transition-duration:2.85s}[data-aos][data-aos][data-aos-delay="2850"],body[data-aos-delay="2850"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2850"].aos-animate,body[data-aos-delay="2850"] [data-aos].aos-animate{transition-delay:2.85s}[data-aos][data-aos][data-aos-duration="2900"],body[data-aos-duration="2900"] [data-aos]{transition-duration:2.9s}[data-aos][data-aos][data-aos-delay="2900"],body[data-aos-delay="2900"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2900"].aos-animate,body[data-aos-delay="2900"] [data-aos].aos-animate{transition-delay:2.9s}[data-aos][data-aos][data-aos-duration="2950"],body[data-aos-duration="2950"] [data-aos]{transition-duration:2.95s}[data-aos][data-aos][data-aos-delay="2950"],body[data-aos-delay="2950"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="2950"].aos-animate,body[data-aos-delay="2950"] [data-aos].aos-animate{transition-delay:2.95s}[data-aos][data-aos][data-aos-duration="3000"],body[data-aos-duration="3000"] [data-aos]{transition-duration:3s}[data-aos][data-aos][data-aos-delay="3000"],body[data-aos-delay="3000"] [data-aos]{transition-delay:0}[data-aos][data-aos][data-aos-delay="3000"].aos-animate,body[data-aos-delay="3000"] [data-aos].aos-animate{transition-delay:3s}[data-aos][data-aos][data-aos-easing=linear],body[data-aos-easing=linear] [data-aos]{transition-timing-function:cubic-bezier(.25,.25,.75,.75)}[data-aos][data-aos][data-aos-easing=ease],body[data-aos-easing=ease] [data-aos]{transition-timing-function:ease}[data-aos][data-aos][data-aos-easing=ease-in],body[data-aos-easing=ease-in] [data-aos]{transition-timing-function:ease-in}[data-aos][data-aos][data-aos-easing=ease-out],body[data-aos-easing=ease-out] [data-aos]{transition-timing-function:ease-out}[data-aos][data-aos][data-aos-easing=ease-in-out],body[data-aos-easing=ease-in-out] [data-aos]{transition-timing-function:ease-in-out}[data-aos][data-aos][data-aos-easing=ease-in-back],body[data-aos-easing=ease-in-back] [data-aos]{transition-timing-function:cubic-bezier(.6,-.28,.735,.045)}[data-aos][data-aos][data-aos-easing=ease-out-back],body[data-aos-easing=ease-out-back] [data-aos]{transition-timing-function:cubic-bezier(.175,.885,.32,1.275)}[data-aos][data-aos][data-aos-easing=ease-in-out-back],body[data-aos-easing=ease-in-out-back] [data-aos]{transition-timing-function:cubic-bezier(.68,-.55,.265,1.55)}[data-aos][data-aos][data-aos-easing=ease-in-sine],body[data-aos-easing=ease-in-sine] [data-aos]{transition-timing-function:cubic-bezier(.47,0,.745,.715)}[data-aos][data-aos][data-aos-easing=ease-out-sine],body[data-aos-easing=ease-out-sine] [data-aos]{transition-timing-function:cubic-bezier(.39,.575,.565,1)}[data-aos][data-aos][data-aos-easing=ease-in-out-sine],body[data-aos-easing=ease-in-out-sine] [data-aos]{transition-timing-function:cubic-bezier(.445,.05,.55,.95)}[data-aos][data-aos][data-aos-easing=ease-in-quad],body[data-aos-easing=ease-in-quad] [data-aos]{transition-timing-function:cubic-bezier(.55,.085,.68,.53)}[data-aos][data-aos][data-aos-easing=ease-out-quad],body[data-aos-easing=ease-out-quad] [data-aos]{transition-timing-function:cubic-bezier(.25,.46,.45,.94)}[data-aos][data-aos][data-aos-easing=ease-in-out-quad],body[data-aos-easing=ease-in-out-quad] [data-aos]{transition-timing-function:cubic-bezier(.455,.03,.515,.955)}[data-aos][data-aos][data-aos-easing=ease-in-cubic],body[data-aos-easing=ease-in-cubic] [data-aos]{transition-timing-function:cubic-bezier(.55,.085,.68,.53)}[data-aos][data-aos][data-aos-easing=ease-out-cubic],body[data-aos-easing=ease-out-cubic] [data-aos]{transition-timing-function:cubic-bezier(.25,.46,.45,.94)}[data-aos][data-aos][data-aos-easing=ease-in-out-cubic],body[data-aos-easing=ease-in-out-cubic] [data-aos]{transition-timing-function:cubic-bezier(.455,.03,.515,.955)}[data-aos][data-aos][data-aos-easing=ease-in-quart],body[data-aos-easing=ease-in-quart] [data-aos]{transition-timing-function:cubic-bezier(.55,.085,.68,.53)}[data-aos][data-aos][data-aos-easing=ease-out-quart],body[data-aos-easing=ease-out-quart] [data-aos]{transition-timing-function:cubic-bezier(.25,.46,.45,.94)}[data-aos][data-aos][data-aos-easing=ease-in-out-quart],body[data-aos-easing=ease-in-out-quart] [data-aos]{transition-timing-function:cubic-bezier(.455,.03,.515,.955)}[data-aos^=fade][data-aos^=fade]{opacity:0;transition-property:opacity,transform}[data-aos^=fade][data-aos^=fade].aos-animate{opacity:1;transform:translateZ(0)}[data-aos=fade-up]{transform:translate3d(0,100px,0)}[data-aos=fade-down]{transform:translate3d(0,-100px,0)}[data-aos=fade-right]{transform:translate3d(-100px,0,0)}[data-aos=fade-left]{transform:translate3d(100px,0,0)}[data-aos=fade-up-right]{transform:translate3d(-100px,100px,0)}[data-aos=fade-up-left]{transform:translate3d(100px,100px,0)}[data-aos=fade-down-right]{transform:translate3d(-100px,-100px,0)}[data-aos=fade-down-left]{transform:translate3d(100px,-100px,0)}[data-aos^=zoom][data-aos^=zoom]{opacity:0;transition-property:opacity,transform}[data-aos^=zoom][data-aos^=zoom].aos-animate{opacity:1;transform:translateZ(0) scale(1)}[data-aos=zoom-in]{transform:scale(.6)}[data-aos=zoom-in-up]{transform:translate3d(0,100px,0) scale(.6)}[data-aos=zoom-in-down]{transform:translate3d(0,-100px,0) scale(.6)}[data-aos=zoom-in-right]{transform:translate3d(-100px,0,0) scale(.6)}[data-aos=zoom-in-left]{transform:translate3d(100px,0,0) scale(.6)}[data-aos=zoom-out]{transform:scale(1.2)}[data-aos=zoom-out-up]{transform:translate3d(0,100px,0) scale(1.2)}[data-aos=zoom-out-down]{transform:translate3d(0,-100px,0) scale(1.2)}[data-aos=zoom-out-right]{transform:translate3d(-100px,0,0) scale(1.2)}[data-aos=zoom-out-left]{transform:translate3d(100px,0,0) scale(1.2)}[data-aos^=slide][data-aos^=slide]{transition-property:transform}[data-aos^=slide][data-aos^=slide].aos-animate{transform:translateZ(0)}[data-aos=slide-up]{transform:translate3d(0,100%,0)}[data-aos=slide-down]{transform:translate3d(0,-100%,0)}[data-aos=slide-right]{transform:translate3d(-100%,0,0)}[data-aos=slide-left]{transform:translate3d(100%,0,0)}[data-aos^=flip][data-aos^=flip]{backface-visibility:hidden;transition-property:transform}[data-aos=flip-left]{transform:perspective(2500px) rotateY(-100deg)}[data-aos=flip-left].aos-animate{transform:perspective(2500px) rotateY(0)}[data-aos=flip-right]{transform:perspective(2500px) rotateY(100deg)}[data-aos=flip-right].aos-animate{transform:perspective(2500px) rotateY(0)}[data-aos=flip-up]{transform:perspective(2500px) rotateX(-100deg)}[data-aos=flip-up].aos-animate{transform:perspective(2500px) rotateX(0)}[data-aos=flip-down]{transform:perspective(2500px) rotateX(100deg)}[data-aos=flip-down].aos-animate{transform:perspective(2500px) rotateX(0)}
|