underscorepy 0.6.0__tar.gz → 0.6.3__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.0 → underscorepy-0.6.3}/MANIFEST.in +1 -1
- {underscorepy-0.6.0/underscorepy.egg-info → underscorepy-0.6.3}/PKG-INFO +4 -1
- underscorepy-0.6.3/README.md +7 -0
- {underscorepy-0.6.0 → underscorepy-0.6.3}/_/__init__.py +10 -13
- {underscorepy-0.6.0 → underscorepy-0.6.3}/_/application.py +66 -44
- underscorepy-0.6.3/_/auth.py +105 -0
- {underscorepy-0.6.0 → underscorepy-0.6.3}/_/caches/__init__.py +17 -5
- underscorepy-0.6.3/_/caches/dbcache.py +117 -0
- underscorepy-0.6.3/_/caches/memory.py +71 -0
- underscorepy-0.6.3/_/caches/redis.py +95 -0
- {underscorepy-0.6.0 → underscorepy-0.6.3}/_/components.py +41 -8
- underscorepy-0.6.3/_/databases/__init__.py +178 -0
- {underscorepy-0.6.0 → underscorepy-0.6.3}/_/databases/postgres.py +8 -15
- {underscorepy-0.6.0 → underscorepy-0.6.3}/_/databases/sqlite.py +61 -37
- underscorepy-0.6.0/_/handlers/protected.py → underscorepy-0.6.3/_/handlers.py +14 -5
- {underscorepy-0.6.0 → underscorepy-0.6.3}/_/logins/__init__.py +30 -8
- underscorepy-0.6.3/_/logins/dblogin.py +194 -0
- {underscorepy-0.6.0 → underscorepy-0.6.3}/_/logins/facebook.py +1 -1
- {underscorepy-0.6.0 → underscorepy-0.6.3}/_/logins/gitlab.py +13 -12
- {underscorepy-0.6.0 → underscorepy-0.6.3}/_/logins/google.py +6 -2
- {underscorepy-0.6.0 → underscorepy-0.6.3}/_/logins/htpasswd.py +1 -1
- {underscorepy-0.6.0 → underscorepy-0.6.3}/_/logins/oauth2.py +17 -3
- {underscorepy-0.6.0 → underscorepy-0.6.3}/_/logins/slap.py +1 -1
- underscorepy-0.6.3/_/records/Protobuf.proto +19 -0
- underscorepy-0.6.3/_/records/Protobuf_pb2.py +35 -0
- underscorepy-0.6.3/_/records/__init__.py +197 -0
- underscorepy-0.6.3/_/records/data.py +186 -0
- underscorepy-0.6.3/_/records/protobuf.py +149 -0
- {underscorepy-0.6.0 → underscorepy-0.6.3}/_/settings.py +32 -31
- {underscorepy-0.6.0 → underscorepy-0.6.3}/_/supports/__init__.py +2 -4
- underscorepy-0.6.0/_/supports/nginx/nginx.py → underscorepy-0.6.3/_/supports/nginx/__init__.py +3 -3
- {underscorepy-0.6.0 → underscorepy-0.6.3}/_/supports/nginx/nginx.conf +2 -2
- underscorepy-0.6.0/_/supports/systemd/systemd.py → underscorepy-0.6.3/_/supports/systemd/__init__.py +3 -3
- {underscorepy-0.6.0 → underscorepy-0.6.3}/_/supports/systemd/systemd.service +1 -1
- underscorepy-0.6.3/_/utils.py +99 -0
- {underscorepy-0.6.0 → underscorepy-0.6.3}/_/version.py +2 -2
- underscorepy-0.6.0/_/websockets/websocket.py → underscorepy-0.6.3/_/websockets.py +18 -1
- {underscorepy-0.6.0 → underscorepy-0.6.3}/setup.py +1 -1
- {underscorepy-0.6.0 → underscorepy-0.6.3/underscorepy.egg-info}/PKG-INFO +4 -1
- underscorepy-0.6.3/underscorepy.egg-info/SOURCES.txt +44 -0
- underscorepy-0.6.0/README.rst +0 -5
- underscorepy-0.6.0/_/__pycache__/__init__.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/__pycache__/application.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/__pycache__/components.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/__pycache__/settings.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/__pycache__/utils.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/__pycache__/version.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/__pycache__/websockets.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/auth/__init__.py +0 -3
- underscorepy-0.6.0/_/auth/__pycache__/__init__.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/auth/__pycache__/basic.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/auth/__pycache__/hash.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/auth/basic.py +0 -39
- underscorepy-0.6.0/_/auth/hash.py +0 -20
- underscorepy-0.6.0/_/caches/__pycache__/__init__.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/caches/__pycache__/dbcache.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/caches/__pycache__/memory.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/caches/__pycache__/redis.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/caches/dbcache.py +0 -47
- underscorepy-0.6.0/_/caches/memory.py +0 -30
- underscorepy-0.6.0/_/caches/redis.py +0 -58
- underscorepy-0.6.0/_/databases/__init__.py +0 -43
- underscorepy-0.6.0/_/databases/__pycache__/__init__.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/databases/__pycache__/sqlite.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/handlers/__init__.py +0 -4
- underscorepy-0.6.0/_/handlers/__pycache__/__init__.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/handlers/__pycache__/login.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/handlers/__pycache__/protected.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/handlers/__pycache__/template.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/handlers/login.py +0 -27
- underscorepy-0.6.0/_/handlers/template.py +0 -19
- underscorepy-0.6.0/_/logins/__pycache__/__init__.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/logins/__pycache__/dblogin.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/logins/__pycache__/gitlab.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/logins/__pycache__/oauth2.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/logins/dblogin.py +0 -99
- underscorepy-0.6.0/_/supports/__pycache__/__init__.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/supports/nginx/__init__.py +0 -1
- underscorepy-0.6.0/_/supports/nginx/__pycache__/__init__.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/supports/nginx/__pycache__/nginx.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/supports/systemd/__init__.py +0 -1
- underscorepy-0.6.0/_/supports/systemd/__pycache__/__init__.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/supports/systemd/__pycache__/systemd.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/utils.py +0 -34
- underscorepy-0.6.0/_/websockets/__init__.py +0 -4
- underscorepy-0.6.0/_/websockets/__pycache__/__init__.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/websockets/__pycache__/mixins.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/websockets/__pycache__/protected.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/websockets/__pycache__/websocket.cpython-311.pyc +0 -0
- underscorepy-0.6.0/_/websockets/mixins.py +0 -20
- underscorepy-0.6.0/_/websockets/protected.py +0 -22
- underscorepy-0.6.0/underscorepy.egg-info/SOURCES.txt +0 -82
- {underscorepy-0.6.0 → underscorepy-0.6.3}/LICENSE +0 -0
- {underscorepy-0.6.0 → underscorepy-0.6.3}/setup.cfg +0 -0
- {underscorepy-0.6.0 → underscorepy-0.6.3}/underscorepy.egg-info/dependency_links.txt +0 -0
- {underscorepy-0.6.0 → underscorepy-0.6.3}/underscorepy.egg-info/not-zip-safe +0 -0
- {underscorepy-0.6.0 → underscorepy-0.6.3}/underscorepy.egg-info/requires.txt +0 -0
- {underscorepy-0.6.0 → underscorepy-0.6.3}/underscorepy.egg-info/top_level.txt +0 -0
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
recursive-include _ *
|
|
2
|
-
include *.
|
|
2
|
+
include *.md
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: underscorepy
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.3
|
|
4
4
|
Summary: _.py library
|
|
5
5
|
Home-page: https://shaw.cx/_.py
|
|
6
6
|
Author: Matthew Shaw
|
|
@@ -12,9 +12,12 @@ Classifier: License :: OSI Approved :: MIT License
|
|
|
12
12
|
Classifier: Topic :: Software Development :: Libraries
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.7
|
|
14
14
|
License-File: LICENSE
|
|
15
|
+
Requires-Dist: tornado
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
underscore.py
|
|
18
19
|
-------------
|
|
19
20
|
|
|
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.
|
|
@@ -6,30 +6,27 @@
|
|
|
6
6
|
# Matthew Shaw <mshaw.cx@gmail.com>
|
|
7
7
|
#
|
|
8
8
|
|
|
9
|
-
import
|
|
9
|
+
import importlib
|
|
10
10
|
import os
|
|
11
|
+
import sys
|
|
11
12
|
|
|
12
13
|
root = os.path.dirname(__file__)
|
|
13
14
|
root = os.path.join(root, '..')
|
|
14
15
|
root = os.path.abspath(root)
|
|
15
16
|
|
|
16
|
-
stop = asyncio.Event()
|
|
17
|
-
|
|
18
|
-
# a generic error class for throwing exceptions
|
|
19
|
-
class error(Exception):
|
|
20
|
-
def __init__(self, fmt, *args):
|
|
21
|
-
self.message = fmt % args
|
|
22
|
-
|
|
23
|
-
def __str__(self):
|
|
24
|
-
return self.message
|
|
25
|
-
|
|
26
17
|
from .utils import *
|
|
27
18
|
|
|
28
19
|
from . import version
|
|
29
20
|
from . import settings
|
|
30
21
|
from . import auth
|
|
31
|
-
from . import components
|
|
32
22
|
from . import handlers
|
|
33
|
-
from . import
|
|
23
|
+
from . import components
|
|
34
24
|
|
|
35
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()
|
|
@@ -22,7 +22,7 @@ import _
|
|
|
22
22
|
class Application(tornado.web.Application):
|
|
23
23
|
@classmethod
|
|
24
24
|
def main(cls, ns=None):
|
|
25
|
-
self = cls()
|
|
25
|
+
_.application = self = cls()
|
|
26
26
|
try:
|
|
27
27
|
asyncio.run(self.__async_main(ns))
|
|
28
28
|
except _.error as e:
|
|
@@ -31,14 +31,21 @@ class Application(tornado.web.Application):
|
|
|
31
31
|
logging.error('%s', e)
|
|
32
32
|
|
|
33
33
|
async def __async_main(self, ns):
|
|
34
|
+
self._records_patterns = []
|
|
35
|
+
self._login_patterns = []
|
|
36
|
+
|
|
37
|
+
self.patterns = []
|
|
38
|
+
self.settings = {}
|
|
39
|
+
|
|
34
40
|
self.loop = asyncio.get_event_loop()
|
|
41
|
+
self._stop_event = asyncio.Event()
|
|
35
42
|
|
|
36
43
|
signal.signal(signal.SIGINT, self.__signalHandler)
|
|
37
44
|
signal.signal(signal.SIGTERM, self.__signalHandler)
|
|
38
45
|
|
|
39
|
-
|
|
46
|
+
name = self.__class__.__name__.lower()
|
|
40
47
|
try:
|
|
41
|
-
await _.settings.load(
|
|
48
|
+
await _.settings.load(ns=ns, name=name)
|
|
42
49
|
await self.logging()
|
|
43
50
|
except _.error as e:
|
|
44
51
|
if _.args.debug:
|
|
@@ -46,17 +53,8 @@ class Application(tornado.web.Application):
|
|
|
46
53
|
logging.error('%s', e)
|
|
47
54
|
self.stop()
|
|
48
55
|
|
|
49
|
-
if not
|
|
50
|
-
for name,component in _.
|
|
51
|
-
try:
|
|
52
|
-
await _.wait(component.args(name))
|
|
53
|
-
except _.error as e:
|
|
54
|
-
logging.error('%s', e)
|
|
55
|
-
self.stop()
|
|
56
|
-
break
|
|
57
|
-
|
|
58
|
-
if not _.stop.is_set():
|
|
59
|
-
for name,component in _.support.items():
|
|
56
|
+
if not self._stop_event.is_set():
|
|
57
|
+
for name,component in _.logins.items():
|
|
60
58
|
try:
|
|
61
59
|
await _.wait(component.args(name))
|
|
62
60
|
except _.error as e:
|
|
@@ -64,32 +62,33 @@ class Application(tornado.web.Application):
|
|
|
64
62
|
self.stop()
|
|
65
63
|
break
|
|
66
64
|
|
|
67
|
-
if not
|
|
65
|
+
if not self._stop_event.is_set():
|
|
68
66
|
try:
|
|
69
67
|
await self.__async_init()
|
|
70
68
|
except _.error as e:
|
|
71
69
|
logging.error('%s', e)
|
|
72
70
|
|
|
73
|
-
for name,component in _.
|
|
71
|
+
for name,component in _.caches.items():
|
|
74
72
|
await component.close()
|
|
75
73
|
|
|
76
|
-
for name,component in _.
|
|
74
|
+
for name,component in _.databases.items():
|
|
77
75
|
await component.close()
|
|
78
76
|
|
|
79
77
|
async def __async_init(self, **kwds):
|
|
80
|
-
self.patterns = []
|
|
81
|
-
self.login_urls = []
|
|
82
|
-
|
|
83
78
|
# check if a sessions cache was specified
|
|
84
|
-
_.sessions = _.config.get(_.
|
|
79
|
+
_.sessions = _.config.get(_.name, 'sessions', fallback=None)
|
|
85
80
|
if _.sessions:
|
|
86
81
|
logging.debug('Sessions cache: %s', _.sessions)
|
|
87
82
|
try:
|
|
88
|
-
_.sessions = _.
|
|
83
|
+
_.sessions = _.caches[_.sessions]
|
|
89
84
|
except KeyError:
|
|
90
85
|
raise _.error('Unknown sessions cache instance: %s', _.sessions)
|
|
91
86
|
|
|
92
|
-
|
|
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
|
|
93
92
|
try:
|
|
94
93
|
await _.wait(self.initialize())
|
|
95
94
|
except NotImplementedError:
|
|
@@ -98,28 +97,34 @@ class Application(tornado.web.Application):
|
|
|
98
97
|
if 'cookie_secret' not in self.settings:
|
|
99
98
|
self.settings['cookie_secret'] = await self.cookie_secret()
|
|
100
99
|
|
|
101
|
-
|
|
102
|
-
self.login_urls.append((f'/login/{instance}', cls))
|
|
100
|
+
patterns = list(self._records_patterns)
|
|
103
101
|
|
|
104
|
-
if self.
|
|
105
|
-
self.
|
|
106
|
-
|
|
107
|
-
( r'/
|
|
108
|
-
|
|
109
|
-
] + self.patterns
|
|
102
|
+
if self._login_patterns:
|
|
103
|
+
self._login_patterns += [
|
|
104
|
+
( r'/login', _.logins.LoginPage ),
|
|
105
|
+
( r'/logout', _.logins.Logout ),
|
|
106
|
+
]
|
|
110
107
|
self.settings['login_url'] = '/login'
|
|
108
|
+
patterns += self._login_patterns
|
|
109
|
+
patterns += self.patterns
|
|
111
110
|
|
|
112
|
-
|
|
111
|
+
patterns.append(
|
|
113
112
|
( r'/(favicon.ico)', tornado.web.StaticFileHandler, {'path':''}),
|
|
114
113
|
)
|
|
115
114
|
|
|
116
|
-
|
|
117
|
-
|
|
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())
|
|
118
123
|
|
|
119
|
-
async def __listen(self, **kwds):
|
|
124
|
+
async def __listen(self, patterns, **kwds):
|
|
120
125
|
# call the Tornado Application init here to give children a chance
|
|
121
126
|
# to initialize patterns and settings
|
|
122
|
-
super(
|
|
127
|
+
super().__init__(patterns, **self.settings)
|
|
123
128
|
|
|
124
129
|
if 'xheaders' not in kwds:
|
|
125
130
|
kwds['xheaders'] = True
|
|
@@ -131,6 +136,16 @@ class Application(tornado.web.Application):
|
|
|
131
136
|
|
|
132
137
|
logging.info('Listening on %s:%d', _.args.address, _.args.port)
|
|
133
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
|
+
|
|
134
149
|
async def initialize(self):
|
|
135
150
|
'underscore apps should override this function'
|
|
136
151
|
raise NotImplementedError
|
|
@@ -139,8 +154,8 @@ class Application(tornado.web.Application):
|
|
|
139
154
|
'underscore apps can override or extend this function'
|
|
140
155
|
|
|
141
156
|
# add the handlers to the logger
|
|
142
|
-
if _.config.getboolean(_.
|
|
143
|
-
full_path = _.paths(f'{_.
|
|
157
|
+
if _.config.getboolean(_.name, 'logging', fallback=False):
|
|
158
|
+
full_path = _.paths(f'{_.name}.log')
|
|
144
159
|
file_logger = logging.FileHandler(full_path)
|
|
145
160
|
file_logger.setLevel(logging.DEBUG if _.args.debug else logging.INFO)
|
|
146
161
|
file_logger.setFormatter(
|
|
@@ -158,9 +173,14 @@ class Application(tornado.web.Application):
|
|
|
158
173
|
return await _.wait(_.sessions.cookie_secret())
|
|
159
174
|
|
|
160
175
|
async def on_login(self, handler, user, *args, **kwds):
|
|
161
|
-
'underscore apps should override this function'
|
|
176
|
+
'underscore apps should override this function if a login is specified'
|
|
162
177
|
raise NotImplementedError
|
|
163
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
|
+
|
|
164
184
|
def periodic(self, _timeout, fn, *args, **kwds):
|
|
165
185
|
'run a function or coroutine on a recurring basis'
|
|
166
186
|
async def _periodic():
|
|
@@ -168,7 +188,7 @@ class Application(tornado.web.Application):
|
|
|
168
188
|
# bail if the stop event is set
|
|
169
189
|
# otherwise run the function after the timeout occurs
|
|
170
190
|
try:
|
|
171
|
-
await asyncio.wait_for(
|
|
191
|
+
await asyncio.wait_for(self._stop_event.wait(), timeout=_timeout)
|
|
172
192
|
break
|
|
173
193
|
except asyncio.TimeoutError as e:
|
|
174
194
|
pass
|
|
@@ -180,17 +200,19 @@ class Application(tornado.web.Application):
|
|
|
180
200
|
|
|
181
201
|
def stop(self):
|
|
182
202
|
logging.debug('Setting stop event')
|
|
183
|
-
|
|
203
|
+
self._stop_event.set()
|
|
204
|
+
|
|
205
|
+
def on_stop(self):
|
|
206
|
+
pass
|
|
184
207
|
|
|
185
208
|
def __signalHandler(self, signum, frame):
|
|
186
209
|
'handle signals in a thread-safe way'
|
|
187
210
|
signame = signal.Signals(signum).name
|
|
188
211
|
handler = getattr(self, f'on_{signame.lower()}', None)
|
|
189
212
|
if handler:
|
|
190
|
-
|
|
191
|
-
if stop:
|
|
213
|
+
if handler(signum, frame):
|
|
192
214
|
return
|
|
193
|
-
logging.info('Terminating %s on %s signal', _.
|
|
215
|
+
logging.info('Terminating %s on %s signal', _.name, signame)
|
|
194
216
|
self.loop.call_soon_threadsafe(self.stop)
|
|
195
217
|
|
|
196
218
|
# demonstrate a specific signal handler
|
|
@@ -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()
|
|
@@ -13,11 +13,19 @@ class Cache:
|
|
|
13
13
|
@classmethod
|
|
14
14
|
async def _(cls, name, **kwds):
|
|
15
15
|
# create a dynamic child class with kwds from the ini file
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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)
|
|
21
29
|
|
|
22
30
|
async def init(self, **kwds):
|
|
23
31
|
pass
|
|
@@ -36,3 +44,7 @@ class Cache:
|
|
|
36
44
|
|
|
37
45
|
async def load_session(self, session_id):
|
|
38
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))
|