underscorepy 0.5.1__tar.gz → 0.6.2__tar.gz
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.
- underscorepy-0.6.2/LICENSE +22 -0
- underscorepy-0.6.2/MANIFEST.in +2 -0
- underscorepy-0.6.2/PKG-INFO +23 -0
- underscorepy-0.6.2/README.md +7 -0
- underscorepy-0.6.2/_/__init__.py +32 -0
- underscorepy-0.6.2/_/application.py +221 -0
- underscorepy-0.6.2/_/auth.py +105 -0
- underscorepy-0.6.2/_/caches/__init__.py +50 -0
- underscorepy-0.6.2/_/caches/dbcache.py +117 -0
- underscorepy-0.6.2/_/caches/memory.py +71 -0
- underscorepy-0.6.2/_/caches/redis.py +95 -0
- underscorepy-0.6.2/_/components.py +100 -0
- underscorepy-0.6.2/_/databases/__init__.py +178 -0
- underscorepy-0.6.2/_/databases/postgres.py +97 -0
- underscorepy-0.6.2/_/databases/sqlite.py +151 -0
- underscorepy-0.6.2/_/handlers.py +40 -0
- underscorepy-0.6.2/_/logins/__init__.py +72 -0
- underscorepy-0.6.2/_/logins/dblogin.py +194 -0
- underscorepy-0.6.2/_/logins/facebook.py +40 -0
- underscorepy-0.6.2/_/logins/gitlab.py +49 -0
- underscorepy-0.6.2/_/logins/google.py +20 -0
- underscorepy-0.5.1/_/web/auth/simple.py → underscorepy-0.6.2/_/logins/htpasswd.py +24 -13
- underscorepy-0.6.2/_/logins/oauth2.py +52 -0
- underscorepy-0.6.2/_/logins/slap.py +60 -0
- underscorepy-0.6.2/_/records/Protobuf.proto +19 -0
- underscorepy-0.6.2/_/records/__init__.py +197 -0
- underscorepy-0.6.2/_/records/data.py +186 -0
- underscorepy-0.6.2/_/records/protobuf.py +149 -0
- underscorepy-0.6.2/_/settings.py +149 -0
- underscorepy-0.6.2/_/supports/__init__.py +29 -0
- underscorepy-0.6.2/_/supports/nginx/__init__.py +58 -0
- underscorepy-0.6.2/_/supports/nginx/nginx.conf +59 -0
- underscorepy-0.6.2/_/supports/systemd/__init__.py +54 -0
- underscorepy-0.6.2/_/supports/systemd/systemd.service +12 -0
- underscorepy-0.6.2/_/utils.py +99 -0
- underscorepy-0.6.2/_/version.py +5 -0
- underscorepy-0.6.2/_/websockets.py +49 -0
- underscorepy-0.6.2/setup.py +29 -0
- underscorepy-0.6.2/underscorepy.egg-info/PKG-INFO +23 -0
- underscorepy-0.6.2/underscorepy.egg-info/SOURCES.txt +43 -0
- underscorepy-0.6.2/underscorepy.egg-info/not-zip-safe +1 -0
- underscorepy-0.6.2/underscorepy.egg-info/requires.txt +1 -0
- underscorepy-0.5.1/MANIFEST.in +0 -1
- underscorepy-0.5.1/PKG-INFO +0 -20
- underscorepy-0.5.1/README.rst +0 -3
- underscorepy-0.5.1/_/__init__.py +0 -22
- underscorepy-0.5.1/_/colors.py +0 -448
- underscorepy-0.5.1/_/components.py +0 -69
- underscorepy-0.5.1/_/decorators.py +0 -10
- underscorepy-0.5.1/_/io.py +0 -62
- underscorepy-0.5.1/_/piper.py +0 -152
- underscorepy-0.5.1/_/settings.py +0 -125
- underscorepy-0.5.1/_/shell.py +0 -371
- underscorepy-0.5.1/_/skel.py +0 -33
- underscorepy-0.5.1/_/storage/__init__.py +0 -16
- underscorepy-0.5.1/_/storage/engines/__init__.py +0 -41
- underscorepy-0.5.1/_/storage/engines/mongo.py +0 -63
- underscorepy-0.5.1/_/storage/engines/postgres.py +0 -151
- underscorepy-0.5.1/_/storage/engines/redis.py +0 -32
- underscorepy-0.5.1/_/storage/engines/sqlite.py +0 -98
- underscorepy-0.5.1/_/storage/records.py +0 -157
- underscorepy-0.5.1/_/version.py +0 -8
- underscorepy-0.5.1/_/web/__init__.py +0 -18
- underscorepy-0.5.1/_/web/application.py +0 -123
- underscorepy-0.5.1/_/web/auth/__init__.py +0 -15
- underscorepy-0.5.1/_/web/auth/basic.py +0 -24
- underscorepy-0.5.1/_/web/auth/facebook.py +0 -69
- underscorepy-0.5.1/_/web/auth/google.py +0 -54
- underscorepy-0.5.1/_/web/auth/logout.py +0 -8
- underscorepy-0.5.1/_/web/auth/slap.py +0 -46
- underscorepy-0.5.1/_/web/handlers/__init__.py +0 -14
- underscorepy-0.5.1/_/web/handlers/index.py +0 -46
- underscorepy-0.5.1/_/web/handlers/ws.py +0 -42
- underscorepy-0.5.1/_/web/util.py +0 -9
- underscorepy-0.5.1/setup.py +0 -40
- underscorepy-0.5.1/underscorepy.egg-info/PKG-INFO +0 -20
- underscorepy-0.5.1/underscorepy.egg-info/SOURCES.txt +0 -38
- underscorepy-0.5.1/underscorepy.egg-info/requires.txt +0 -2
- {underscorepy-0.5.1 → underscorepy-0.6.2}/setup.cfg +0 -0
- {underscorepy-0.5.1 → underscorepy-0.6.2}/underscorepy.egg-info/dependency_links.txt +0 -0
- {underscorepy-0.5.1 → underscorepy-0.6.2}/underscorepy.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2015-2023 Matthew Shaw
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: underscorepy
|
|
3
|
+
Version: 0.6.2
|
|
4
|
+
Summary: _.py library
|
|
5
|
+
Home-page: https://shaw.cx/_.py
|
|
6
|
+
Author: Matthew Shaw
|
|
7
|
+
Author-email: mshaw.cx@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.7
|
|
14
|
+
License-File: LICENSE
|
|
15
|
+
Requires-Dist: tornado
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
underscore.py
|
|
19
|
+
-------------
|
|
20
|
+
|
|
21
|
+
This is a framework for me that has morphed overtime. Some "features" are broken because I have not needed them for my own projects for quite some time. Eventually those bugs will be resolved.
|
|
22
|
+
|
|
23
|
+
For anyone confused by Python files like `_/databases/__databases__.py` I wrote a custom import loader that looks for `__dirname__.py` files before `__init__.py` because I find it easier to track the files that way. Very experimental, and probably fragile, but maybe I'll submit a PEP for it someday.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
|
|
2
|
+
underscore.py
|
|
3
|
+
-------------
|
|
4
|
+
|
|
5
|
+
This is a framework for me that has morphed overtime. Some "features" are broken because I have not needed them for my own projects for quite some time. Eventually those bugs will be resolved.
|
|
6
|
+
|
|
7
|
+
For anyone confused by Python files like `_/databases/__databases__.py` I wrote a custom import loader that looks for `__dirname__.py` files before `__init__.py` because I find it easier to track the files that way. Very experimental, and probably fragile, but maybe I'll submit a PEP for it someday.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#
|
|
2
|
+
# (c) 2015-2023 Matthew Shaw
|
|
3
|
+
#
|
|
4
|
+
# Authors
|
|
5
|
+
# =======
|
|
6
|
+
# Matthew Shaw <mshaw.cx@gmail.com>
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
import importlib
|
|
10
|
+
import os
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
root = os.path.dirname(__file__)
|
|
14
|
+
root = os.path.join(root, '..')
|
|
15
|
+
root = os.path.abspath(root)
|
|
16
|
+
|
|
17
|
+
from .utils import *
|
|
18
|
+
|
|
19
|
+
from . import version
|
|
20
|
+
from . import settings
|
|
21
|
+
from . import auth
|
|
22
|
+
from . import handlers
|
|
23
|
+
from . import components
|
|
24
|
+
|
|
25
|
+
from .application import Application
|
|
26
|
+
|
|
27
|
+
# placeholder for comonents
|
|
28
|
+
caches = Container()
|
|
29
|
+
databases = Container()
|
|
30
|
+
logins = Container()
|
|
31
|
+
records = Container()
|
|
32
|
+
supports = Container()
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#
|
|
2
|
+
# (c) 2015-2023 Matthew Shaw
|
|
3
|
+
#
|
|
4
|
+
# Authors
|
|
5
|
+
# =======
|
|
6
|
+
# Matthew Shaw <mshaw.cx@gmail.com>
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
import asyncio
|
|
10
|
+
import logging
|
|
11
|
+
import os
|
|
12
|
+
import signal
|
|
13
|
+
import socket
|
|
14
|
+
import sys
|
|
15
|
+
import traceback
|
|
16
|
+
|
|
17
|
+
import tornado.web
|
|
18
|
+
|
|
19
|
+
import _
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Application(tornado.web.Application):
|
|
23
|
+
@classmethod
|
|
24
|
+
def main(cls, ns=None):
|
|
25
|
+
_.application = self = cls()
|
|
26
|
+
try:
|
|
27
|
+
asyncio.run(self.__async_main(ns))
|
|
28
|
+
except _.error as e:
|
|
29
|
+
if _.args.debug:
|
|
30
|
+
traceback.print_tb(e.__traceback__)
|
|
31
|
+
logging.error('%s', e)
|
|
32
|
+
|
|
33
|
+
async def __async_main(self, ns):
|
|
34
|
+
self._records_patterns = []
|
|
35
|
+
self._login_patterns = []
|
|
36
|
+
|
|
37
|
+
self.patterns = []
|
|
38
|
+
self.settings = {}
|
|
39
|
+
|
|
40
|
+
self.loop = asyncio.get_event_loop()
|
|
41
|
+
self._stop_event = asyncio.Event()
|
|
42
|
+
|
|
43
|
+
signal.signal(signal.SIGINT, self.__signalHandler)
|
|
44
|
+
signal.signal(signal.SIGTERM, self.__signalHandler)
|
|
45
|
+
|
|
46
|
+
name = self.__class__.__name__.lower()
|
|
47
|
+
try:
|
|
48
|
+
await _.settings.load(ns=ns, name=name)
|
|
49
|
+
await self.logging()
|
|
50
|
+
except _.error as e:
|
|
51
|
+
if _.args.debug:
|
|
52
|
+
traceback.print_tb(e.__traceback__)
|
|
53
|
+
logging.error('%s', e)
|
|
54
|
+
self.stop()
|
|
55
|
+
|
|
56
|
+
if not self._stop_event.is_set():
|
|
57
|
+
for name,component in _.logins.items():
|
|
58
|
+
try:
|
|
59
|
+
await _.wait(component.args(name))
|
|
60
|
+
except _.error as e:
|
|
61
|
+
logging.error('%s', e)
|
|
62
|
+
self.stop()
|
|
63
|
+
break
|
|
64
|
+
|
|
65
|
+
if not self._stop_event.is_set():
|
|
66
|
+
try:
|
|
67
|
+
await self.__async_init()
|
|
68
|
+
except _.error as e:
|
|
69
|
+
logging.error('%s', e)
|
|
70
|
+
|
|
71
|
+
for name,component in _.caches.items():
|
|
72
|
+
await component.close()
|
|
73
|
+
|
|
74
|
+
for name,component in _.databases.items():
|
|
75
|
+
await component.close()
|
|
76
|
+
|
|
77
|
+
async def __async_init(self, **kwds):
|
|
78
|
+
# check if a sessions cache was specified
|
|
79
|
+
_.sessions = _.config.get(_.name, 'sessions', fallback=None)
|
|
80
|
+
if _.sessions:
|
|
81
|
+
logging.debug('Sessions cache: %s', _.sessions)
|
|
82
|
+
try:
|
|
83
|
+
_.sessions = _.caches[_.sessions]
|
|
84
|
+
except KeyError:
|
|
85
|
+
raise _.error('Unknown sessions cache instance: %s', _.sessions)
|
|
86
|
+
|
|
87
|
+
self.settings['static_path'] = _.paths('static')
|
|
88
|
+
self.settings['template_path'] = _.paths('templates')
|
|
89
|
+
self.settings['debug'] = _.args.debug
|
|
90
|
+
|
|
91
|
+
# call the underscore application's entry point
|
|
92
|
+
try:
|
|
93
|
+
await _.wait(self.initialize())
|
|
94
|
+
except NotImplementedError:
|
|
95
|
+
logging.warning('No "initialize" function defined')
|
|
96
|
+
|
|
97
|
+
if 'cookie_secret' not in self.settings:
|
|
98
|
+
self.settings['cookie_secret'] = await self.cookie_secret()
|
|
99
|
+
|
|
100
|
+
patterns = list(self._records_patterns)
|
|
101
|
+
|
|
102
|
+
if self._login_patterns:
|
|
103
|
+
self._login_patterns += [
|
|
104
|
+
( r'/login', _.logins.LoginPage ),
|
|
105
|
+
( r'/logout', _.logins.Logout ),
|
|
106
|
+
]
|
|
107
|
+
self.settings['login_url'] = '/login'
|
|
108
|
+
patterns += self._login_patterns
|
|
109
|
+
patterns += self.patterns
|
|
110
|
+
|
|
111
|
+
patterns.append(
|
|
112
|
+
( r'/(favicon.ico)', tornado.web.StaticFileHandler, {'path':''}),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
if _.args.debug:
|
|
116
|
+
for (pattern,cls,*params) in patterns:
|
|
117
|
+
handler = f'{cls.__module__}.{cls.__name__}'
|
|
118
|
+
logging.debug('%-32s %s %s', pattern, handler, params[0] if params else '')
|
|
119
|
+
|
|
120
|
+
await self.__listen(patterns)
|
|
121
|
+
await self._stop_event.wait()
|
|
122
|
+
await _.wait(self.on_stop())
|
|
123
|
+
|
|
124
|
+
async def __listen(self, patterns, **kwds):
|
|
125
|
+
# call the Tornado Application init here to give children a chance
|
|
126
|
+
# to initialize patterns and settings
|
|
127
|
+
super().__init__(patterns, **self.settings)
|
|
128
|
+
|
|
129
|
+
if 'xheaders' not in kwds:
|
|
130
|
+
kwds['xheaders'] = True
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
self.listen(_.args.port, _.args.address, **kwds)
|
|
134
|
+
except Exception as e:
|
|
135
|
+
raise _.error('%s', e) from None
|
|
136
|
+
|
|
137
|
+
logging.info('Listening on %s:%d', _.args.address, _.args.port)
|
|
138
|
+
|
|
139
|
+
def _record_handler(self, name, cls):
|
|
140
|
+
self._records_patterns.append(
|
|
141
|
+
(f'/{name}/{cls._name}(?:/(.*))?', cls)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _login_handler(self, name, cls):
|
|
145
|
+
self._login_patterns.append(
|
|
146
|
+
(f'/{name}/{cls._name}', cls)
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
async def initialize(self):
|
|
150
|
+
'underscore apps should override this function'
|
|
151
|
+
raise NotImplementedError
|
|
152
|
+
|
|
153
|
+
async def logging(self):
|
|
154
|
+
'underscore apps can override or extend this function'
|
|
155
|
+
|
|
156
|
+
# add the handlers to the logger
|
|
157
|
+
if _.config.getboolean(_.name, 'logging', fallback=False):
|
|
158
|
+
full_path = _.paths(f'{_.name}.log')
|
|
159
|
+
file_logger = logging.FileHandler(full_path)
|
|
160
|
+
file_logger.setLevel(logging.DEBUG if _.args.debug else logging.INFO)
|
|
161
|
+
file_logger.setFormatter(
|
|
162
|
+
logging.Formatter(
|
|
163
|
+
fmt = '%(asctime)s %(levelname)-8s %(message)s',
|
|
164
|
+
datefmt = '%Y-%m-%d %H:%M:%S',
|
|
165
|
+
)
|
|
166
|
+
)
|
|
167
|
+
root_logger = logging.getLogger()
|
|
168
|
+
root_logger.addHandler(file_logger)
|
|
169
|
+
|
|
170
|
+
async def cookie_secret(self):
|
|
171
|
+
'underscore apps can override this function'
|
|
172
|
+
if _.sessions is not None:
|
|
173
|
+
return await _.wait(_.sessions.cookie_secret())
|
|
174
|
+
|
|
175
|
+
async def on_login(self, handler, user, *args, **kwds):
|
|
176
|
+
'underscore apps should override this function if a login is specified'
|
|
177
|
+
raise NotImplementedError
|
|
178
|
+
|
|
179
|
+
def task(self, fn, *args, **kwds):
|
|
180
|
+
async def _task():
|
|
181
|
+
await _.wait(fn(*args, **kwds))
|
|
182
|
+
return asyncio.create_task(_task())
|
|
183
|
+
|
|
184
|
+
def periodic(self, _timeout, fn, *args, **kwds):
|
|
185
|
+
'run a function or coroutine on a recurring basis'
|
|
186
|
+
async def _periodic():
|
|
187
|
+
while True:
|
|
188
|
+
# bail if the stop event is set
|
|
189
|
+
# otherwise run the function after the timeout occurs
|
|
190
|
+
try:
|
|
191
|
+
await asyncio.wait_for(self._stop_event.wait(), timeout=_timeout)
|
|
192
|
+
break
|
|
193
|
+
except asyncio.TimeoutError as e:
|
|
194
|
+
pass
|
|
195
|
+
try:
|
|
196
|
+
result = await _.wait(fn(*args, **kwds))
|
|
197
|
+
except Exception as e:
|
|
198
|
+
logging.exception(e)
|
|
199
|
+
return asyncio.create_task(_periodic())
|
|
200
|
+
|
|
201
|
+
def stop(self):
|
|
202
|
+
logging.debug('Setting stop event')
|
|
203
|
+
self._stop_event.set()
|
|
204
|
+
|
|
205
|
+
def on_stop(self):
|
|
206
|
+
pass
|
|
207
|
+
|
|
208
|
+
def __signalHandler(self, signum, frame):
|
|
209
|
+
'handle signals in a thread-safe way'
|
|
210
|
+
signame = signal.Signals(signum).name
|
|
211
|
+
handler = getattr(self, f'on_{signame.lower()}', None)
|
|
212
|
+
if handler:
|
|
213
|
+
if handler(signum, frame):
|
|
214
|
+
return
|
|
215
|
+
logging.info('Terminating %s on %s signal', _.name, signame)
|
|
216
|
+
self.loop.call_soon_threadsafe(self.stop)
|
|
217
|
+
|
|
218
|
+
# demonstrate a specific signal handler
|
|
219
|
+
# and print newline after ^C on terminals
|
|
220
|
+
def on_sigint(self, signum, frame):
|
|
221
|
+
print()
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#
|
|
2
|
+
# (c) 2015-2023 Matthew Shaw
|
|
3
|
+
#
|
|
4
|
+
# Authors
|
|
5
|
+
# =======
|
|
6
|
+
# Matthew Shaw <mshaw.cx@gmail.com>
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import binascii
|
|
11
|
+
import functools
|
|
12
|
+
import hashlib
|
|
13
|
+
import urllib.parse
|
|
14
|
+
|
|
15
|
+
import _
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# based on tornado.web.authenticated
|
|
19
|
+
def _next(handler):
|
|
20
|
+
if handler.request.method in ("GET","HEAD"):
|
|
21
|
+
url = handler.get_login_url()
|
|
22
|
+
if "?" not in url:
|
|
23
|
+
if urllib.parse.urlsplit(url).scheme:
|
|
24
|
+
next_url = handler.request.full_url()
|
|
25
|
+
else:
|
|
26
|
+
next_url = handler.request.uri
|
|
27
|
+
url += "?" + urllib.parse.urlencode(dict(next=next_url))
|
|
28
|
+
handler.redirect(url)
|
|
29
|
+
else:
|
|
30
|
+
raise _.HTTPError(403)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# generic decorator to callback to a function
|
|
34
|
+
# return true to allow the method to be called
|
|
35
|
+
# return false to block the method
|
|
36
|
+
def filter(filter_func):
|
|
37
|
+
def _filter(method):
|
|
38
|
+
@functools.wraps(method)
|
|
39
|
+
def wrapper(self, *args, **kwds):
|
|
40
|
+
if filter_func(self):
|
|
41
|
+
return method(self, *args, **kwds)
|
|
42
|
+
_next(self)
|
|
43
|
+
return wrapper
|
|
44
|
+
return _filter
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# same as above but pass the current_user session to the function
|
|
48
|
+
# blocks when no session is associated with the request
|
|
49
|
+
# return true to allow the method to be called
|
|
50
|
+
# return false to block the method
|
|
51
|
+
def filter_user(filter_func):
|
|
52
|
+
def _filter_user(method):
|
|
53
|
+
@functools.wraps(method)
|
|
54
|
+
def wrapper(self, *args, **kwds):
|
|
55
|
+
if self.current_user:
|
|
56
|
+
if filter_func(self.current_user):
|
|
57
|
+
return method(self, *args, **kwds)
|
|
58
|
+
_next(self)
|
|
59
|
+
return wrapper
|
|
60
|
+
return _filter_user
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# inteded to be used by underscore apps to check that any user is logged in
|
|
64
|
+
current_user = filter_user(lambda current_user: True)
|
|
65
|
+
|
|
66
|
+
# intended to be over-ridden before components are loaded for more fine-grain
|
|
67
|
+
# control over access to component handlers
|
|
68
|
+
protected = filter_user(lambda current_user: True)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# implement old school user/name password prompt
|
|
72
|
+
# useful for command-line tools to pass as part of the URL
|
|
73
|
+
def basic(realm='Authentication'):
|
|
74
|
+
def basic_auth(method):
|
|
75
|
+
@functools.wraps(method)
|
|
76
|
+
async def wrapper(self, *args, **kwds):
|
|
77
|
+
auth = self.request.headers.get('Authorization', '')
|
|
78
|
+
if auth.startswith('Basic '):
|
|
79
|
+
auth = binascii.a2b_base64(auth[6:]).decode('utf-8')
|
|
80
|
+
username,password = auth.split(':', 1)
|
|
81
|
+
component = _.config.get(_.name, 'basic', fallback=None)
|
|
82
|
+
if not component:
|
|
83
|
+
raise _.HTTPError(500, 'No component specified for basic auth')
|
|
84
|
+
try:
|
|
85
|
+
login = _.logins[component]
|
|
86
|
+
except KeyError:
|
|
87
|
+
raise _.HTTPError(500, 'Invalid component specified for basic auth')
|
|
88
|
+
success = await login.check(username, password)
|
|
89
|
+
if success:
|
|
90
|
+
return method(self, *args, **kwds)
|
|
91
|
+
self.set_status(401)
|
|
92
|
+
self.set_header('WWW-Authenticate', f'Basic realm={realm}')
|
|
93
|
+
self.finish()
|
|
94
|
+
return wrapper
|
|
95
|
+
return basic_auth
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def simple_hash(value):
|
|
99
|
+
value = value.encode('utf-8')
|
|
100
|
+
for i in range(99999):
|
|
101
|
+
value = hashlib.sha512(value).digest()
|
|
102
|
+
return base64.b64encode(value).decode('ascii')
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = _.all()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#
|
|
2
|
+
# (c) 2015-2023 Matthew Shaw
|
|
3
|
+
#
|
|
4
|
+
# Authors
|
|
5
|
+
# =======
|
|
6
|
+
# Matthew Shaw <mshaw.cx@gmail.com>
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
import _
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Cache:
|
|
13
|
+
@classmethod
|
|
14
|
+
async def _(cls, name, **kwds):
|
|
15
|
+
# create a dynamic child class with kwds from the ini file
|
|
16
|
+
try:
|
|
17
|
+
members = dict(_.config['sessions'])
|
|
18
|
+
except KeyError:
|
|
19
|
+
members = {}
|
|
20
|
+
|
|
21
|
+
members['expires'] = int(members.get('expires', 24))
|
|
22
|
+
members['interval'] = int(members.get('interval', 60))
|
|
23
|
+
members.update(kwds)
|
|
24
|
+
|
|
25
|
+
# instantiate the derived class
|
|
26
|
+
self = type(cls.__name__, (cls,), _.prefix(members))()
|
|
27
|
+
_.caches[name] = self
|
|
28
|
+
await self.init(name, **kwds)
|
|
29
|
+
|
|
30
|
+
async def init(self, **kwds):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
async def close(self):
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
async def cookie_secret(self):
|
|
37
|
+
raise NotImplementedError
|
|
38
|
+
|
|
39
|
+
def save_session(self, session):
|
|
40
|
+
try:
|
|
41
|
+
return session['session_id']
|
|
42
|
+
except KeyError:
|
|
43
|
+
raise _.error('No session_id defined in session')
|
|
44
|
+
|
|
45
|
+
async def load_session(self, session_id):
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Handler(_.handlers.Protected):
|
|
50
|
+
pass
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#
|
|
2
|
+
# (c) 2015-2023 Matthew Shaw
|
|
3
|
+
#
|
|
4
|
+
# Authors
|
|
5
|
+
# =======
|
|
6
|
+
# Matthew Shaw <mshaw.cx@gmail.com>
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
|
|
14
|
+
import _
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class DbCache(_.caches.Cache):
|
|
18
|
+
_config = 'config'
|
|
19
|
+
_key_col = 'key'
|
|
20
|
+
_key = 'cookie'
|
|
21
|
+
_val_col = 'value'
|
|
22
|
+
|
|
23
|
+
_table = 'sessions'
|
|
24
|
+
_session_id = 'session_id'
|
|
25
|
+
|
|
26
|
+
async def init(self, name, database=None, table=None, **kwds):
|
|
27
|
+
if not hasattr(_.application, 'is_session_expired'):
|
|
28
|
+
raise _.error('Application does not have is_session_expired function defined')
|
|
29
|
+
|
|
30
|
+
if database is None:
|
|
31
|
+
if 1 == len(_.databases):
|
|
32
|
+
database = list(_.databases.keys())[0]
|
|
33
|
+
else:
|
|
34
|
+
raise _.error('dbcache requires a database to be specified')
|
|
35
|
+
self.db = _.databases[database]
|
|
36
|
+
|
|
37
|
+
schema = self.db.schema('config')
|
|
38
|
+
table = schema.table(self._config)
|
|
39
|
+
table.column(self._key_col).primary_key()
|
|
40
|
+
table.column(self._val_col)
|
|
41
|
+
await schema.apply()
|
|
42
|
+
|
|
43
|
+
schema = self.db.schema('sessions')
|
|
44
|
+
table = schema.table(self._table)
|
|
45
|
+
table.column(self._session_id).primary_key()
|
|
46
|
+
for col,dbtype in kwds.items():
|
|
47
|
+
table.column(col).type(dbtype)
|
|
48
|
+
await schema.apply()
|
|
49
|
+
|
|
50
|
+
# interval comes from the [sessions] section of the ini
|
|
51
|
+
_.application.periodic(self._interval, self.clear_stale_sessions)
|
|
52
|
+
|
|
53
|
+
members = dict(
|
|
54
|
+
name = name,
|
|
55
|
+
db = self.db,
|
|
56
|
+
table = self._table,
|
|
57
|
+
session_id = self._session_id,
|
|
58
|
+
)
|
|
59
|
+
subclass = type(name, (DbCacheSessions,), _.prefix(members))
|
|
60
|
+
_.application._record_handler('sessions', subclass)
|
|
61
|
+
|
|
62
|
+
async def cookie_secret(self):
|
|
63
|
+
secret = await self.db.find_one(self._config, self._key_col, self._key)
|
|
64
|
+
if secret:
|
|
65
|
+
secret = secret['value']
|
|
66
|
+
else:
|
|
67
|
+
secret = base64.b64encode(os.urandom(32))
|
|
68
|
+
record = {
|
|
69
|
+
self._key_col : self._key,
|
|
70
|
+
self._val_col : secret,
|
|
71
|
+
}
|
|
72
|
+
await self.db.upsert(self._config, record)
|
|
73
|
+
return secret
|
|
74
|
+
|
|
75
|
+
async def save_session(self, session):
|
|
76
|
+
super().save_session(session)
|
|
77
|
+
await self.db.upsert(self._table, session)
|
|
78
|
+
|
|
79
|
+
async def load_session(self, session_id):
|
|
80
|
+
record = await self.db.find_one(self._table, self._session_id, session_id)
|
|
81
|
+
if not record:
|
|
82
|
+
return None
|
|
83
|
+
if await _.wait(_.application.is_session_expired(record, self._expires)):
|
|
84
|
+
return None
|
|
85
|
+
return record
|
|
86
|
+
|
|
87
|
+
async def clear_stale_sessions(self):
|
|
88
|
+
for record in await self.db.find(self._table):
|
|
89
|
+
if await _.wait(_.application.is_session_expired(record, self._expires)):
|
|
90
|
+
logging.debug('Removing expired session: %s', record[self._session_id])
|
|
91
|
+
await self.db.delete(self._table, self._session_id, record[self._session_id])
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class DbCacheSessions(_.handlers.Protected):
|
|
95
|
+
@_.auth.protected
|
|
96
|
+
async def get(self, session_id=None):
|
|
97
|
+
if session_id:
|
|
98
|
+
record = await self._db.find_one(self._table, self._session_id, session_id)
|
|
99
|
+
self.write(record)
|
|
100
|
+
else:
|
|
101
|
+
records = await self._db.find(self._table)
|
|
102
|
+
data = []
|
|
103
|
+
for record in records:
|
|
104
|
+
data.append(dict(record))
|
|
105
|
+
self.write({'data':data})
|
|
106
|
+
|
|
107
|
+
@_.auth.protected
|
|
108
|
+
async def delete(self, session_id=None):
|
|
109
|
+
self.set_status(204)
|
|
110
|
+
if session_id:
|
|
111
|
+
await self._db.delete(self._table, self._session_id, session_id)
|
|
112
|
+
|
|
113
|
+
callback = getattr(_.application, f'on_{self._name}_delete', None)
|
|
114
|
+
if callback is None:
|
|
115
|
+
callback = getattr(_.application, 'on_dbcache_delete', None)
|
|
116
|
+
if callback:
|
|
117
|
+
await _.wait(callback(self._name, record))
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#
|
|
2
|
+
# (c) 2015-2023 Matthew Shaw
|
|
3
|
+
#
|
|
4
|
+
# Authors
|
|
5
|
+
# =======
|
|
6
|
+
# Matthew Shaw <mshaw.cx@gmail.com>
|
|
7
|
+
#
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import json
|
|
11
|
+
|
|
12
|
+
import _
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Memory(_.caches.Cache):
|
|
16
|
+
async def init(self, name, **kwds):
|
|
17
|
+
if not hasattr(_.application, 'is_session_expired'):
|
|
18
|
+
raise _.error('Application does not have is_session_expired function defined')
|
|
19
|
+
|
|
20
|
+
self.mem = {}
|
|
21
|
+
|
|
22
|
+
members = dict(
|
|
23
|
+
name = name,
|
|
24
|
+
mem = self.mem,
|
|
25
|
+
)
|
|
26
|
+
subclass = type(name, (MemorySessions,), _.prefix(members))
|
|
27
|
+
_.application._record_handler('sessions', subclass)
|
|
28
|
+
|
|
29
|
+
def cookie_secret(self):
|
|
30
|
+
return os.urandom(32)
|
|
31
|
+
|
|
32
|
+
def save_session(self, session):
|
|
33
|
+
session_id = super().save_session(session)
|
|
34
|
+
self.mem[session_id] = json.dumps(session)
|
|
35
|
+
|
|
36
|
+
async def load_session(self, session_id):
|
|
37
|
+
session = self.mem.get(session_id, None)
|
|
38
|
+
if not session:
|
|
39
|
+
return None
|
|
40
|
+
if await _.wait(_.application.is_session_expired(session, self.expires)):
|
|
41
|
+
return None
|
|
42
|
+
return json.loads(session)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class MemorySessions(_.handlers.Protected):
|
|
47
|
+
@_.auth.protected
|
|
48
|
+
async def get(self, session_id=None):
|
|
49
|
+
if session_id:
|
|
50
|
+
session = self._mem[session_id]
|
|
51
|
+
if not session:
|
|
52
|
+
raise _.HTTPError(404)
|
|
53
|
+
session = json.loads(session)
|
|
54
|
+
self.write(session)
|
|
55
|
+
else:
|
|
56
|
+
data = []
|
|
57
|
+
for session_id in self._mem:
|
|
58
|
+
session = self._mem[session_id]
|
|
59
|
+
data.append(json.loads(session))
|
|
60
|
+
self.write({'data':data})
|
|
61
|
+
|
|
62
|
+
@_.auth.protected
|
|
63
|
+
async def delete(self, session_id=None):
|
|
64
|
+
self.set_status(204)
|
|
65
|
+
if session_id:
|
|
66
|
+
del self._mem[session_id]
|
|
67
|
+
callback = getattr(_.application, f'on_{self._name}_delete', None)
|
|
68
|
+
if callback is None:
|
|
69
|
+
callback = getattr(_.application, 'on_memory_delete', None)
|
|
70
|
+
if callback:
|
|
71
|
+
await _.wait(callback(self._name, session_id))
|