Skip to content

Commit 14d7c1b

Browse files
committed
Upgrade deps to get it running on python 3.13
Some hacks on cgi and crypt, which have been removed in core. Some checks performed, but needs more testing. Some things can be serialized wrongly. Before serializing an entity would end up as the ID, now it will end up in an error, but most of those places can be changed to `.id` to get it similar to earlier.
1 parent 44e017a commit 14d7c1b

File tree

6 files changed

+176
-33
lines changed

6 files changed

+176
-33
lines changed

requirements.txt

Lines changed: 28 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,62 +7,62 @@
77

88
attrs==20.2.0
99
bcrypt==3.1.7
10-
blinker==1.4
10+
blinker==1.8.2
1111
cachetools==4.1.1
1212
certifi==2020.6.20
1313
cffi==1.14.5
1414
chardet==3.0.4
1515
charset-normalizer==2.0.3
16-
click==7.1.2
16+
click==8.1.7
1717
cssselect==1.1.0
1818
cssutils==1.0.2
1919
cycler==0.10.0
2020
emails==0.6
21-
Flask==1.1.2
22-
Flask-Bcrypt==0.7.1
23-
Flask-Bower==1.3.0
21+
Flask==3.0.3
22+
Flask-Bcrypt==1.0.1
23+
Flask-Bower==2.0.0
2424
Flask-Env==2.0.0
25-
Flask-Inputs==0.3.0
26-
Flask-Login==0.5.0
27-
Flask-SQLAlchemy==2.4.4
28-
Flask-Testing==0.8.0
29-
future==0.18.2
25+
Flask-Inputs==1.0.0
26+
Flask-Login==0.6.3
27+
Flask-SQLAlchemy==3.1.1
28+
Flask-Testing==0.8.1
29+
future==1.0.0
3030
gunicorn==20.0.4
3131
idna==2.10
3232
iniconfig==1.1.1
33-
itsdangerous==1.1.0
34-
Jinja2==2.11.3
33+
itsdangerous==2.2.0
34+
Jinja2==3.1.4
3535
jsonschema==3.2.0
36-
kiwisolver==1.3.2
36+
kiwisolver==1.4.7
3737
ldif3==3.2.2
38-
lxml==4.9.1
39-
MarkupSafe==1.1.1
40-
matplotlib==3.4.3
38+
lxml==5.3.0
39+
MarkupSafe==3.0.1
40+
matplotlib==3.9.2
4141
nose==1.3.7
42-
numpy==1.23.1
42+
numpy==2.1.2
4343
packaging==21.0
4444
paho-mqtt==1.5.1
45-
pandas==1.3.5
46-
Pillow==9.0.1
45+
pandas==2.2.3
46+
Pillow==11.0.0
4747
pluggy==1.0.0
4848
premailer==3.7.0
49-
psycopg2-binary==2.9.5
49+
psycopg2-binary==2.9.10
5050
py==1.10.0
5151
pycparser==2.20
5252
pyparsing==2.4.7
5353
pyrsistent==0.17.3
5454
pytest==6.2.5
55-
python-dateutil==2.8.1
55+
python-dateutil==2.8.2
5656
pytz==2021.1
57-
PyYAML==5.4
57+
PyYAML==6.0.2
5858
requests==2.26.0
59-
six==1.15.0
60-
SQLAlchemy==1.3.20
61-
SQLAlchemy-Continuum==1.3.11
62-
SQLAlchemy-Utils==0.36.8
59+
six==1.16.0
60+
SQLAlchemy==2.0.36
61+
SQLAlchemy-Continuum==1.4.2
62+
SQLAlchemy-Utils==0.41.2
6363
stripe==5.5.0
6464
toml==0.10.2
6565
typing==3.7.4.3
6666
urllib3==1.26.6
67-
Werkzeug==1.0.1
68-
WTForms==2.3.3
67+
Werkzeug==3.0.4
68+
WTForms==3.1.2

web/src/cgi.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
2+
from email.message import Message
3+
4+
def parse_header(header):
5+
msg = Message()
6+
msg.add_header('content-type', header)
7+
r = msg.get_param('charset')
8+
if r:
9+
return r

web/src/crypt.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
# SHAcrypt using SHA-512, after https://akkadia.org/drepper/SHA-crypt.txt.
2+
#
3+
# Copyright © 2024 Tony Garnock-Jones.
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+
23+
import hashlib
24+
import secrets
25+
26+
alphabet = \
27+
[ord(c) for c in './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz']
28+
permutation = [
29+
[0, 21, 42], [22, 43, 1], [44, 2, 23], [3, 24, 45],
30+
[25, 46, 4], [47, 5, 26], [6, 27, 48], [28, 49, 7],
31+
[50, 8, 29], [9, 30, 51], [31, 52, 10], [53, 11, 32],
32+
[12, 33, 54], [34, 55, 13], [56, 14, 35], [15, 36, 57],
33+
[37, 58, 16], [59, 17, 38], [18, 39, 60], [40, 61, 19],
34+
[62, 20, 41], [-1, -1, 63],
35+
]
36+
def encode(bs64):
37+
result = bytearray(4 * len(permutation))
38+
i = 0
39+
for group in permutation:
40+
g = lambda j: bs64[j] if j != -1 else 0
41+
bits = g(group[0]) << 16 | g(group[1]) << 8 | g(group[2])
42+
result[i] = alphabet[bits & 63]
43+
result[i+1] = alphabet[(bits >> 6) & 63]
44+
result[i+2] = alphabet[(bits >> 12) & 63]
45+
result[i+3] = alphabet[(bits >> 18) & 63]
46+
i = i + 4
47+
return bytes(result).decode('ascii')[:-2]
48+
49+
def repeats_of(n, bs): return bs * int(n / len(bs)) + bs[:n % len(bs)]
50+
def digest(bs): return hashlib.sha512(bs).digest()
51+
52+
def crypt(password, salt = None, rounds = 5000):
53+
if salt is None: salt = encode(secrets.token_bytes(64))[:16].encode('ascii')
54+
salt = salt[:16]
55+
56+
B = digest(password + salt + password)
57+
Ainput = password + salt + repeats_of(len(password), B)
58+
v = len(password)
59+
while v > 0:
60+
Ainput = Ainput + (B if v & 1 else password)
61+
v = v >> 1
62+
A = digest(Ainput)
63+
64+
DP = digest(password * len(password))
65+
P = repeats_of(len(password), DP)
66+
DS = digest(salt * (16+A[0]))
67+
S = repeats_of(len(salt), DS)
68+
69+
C = A
70+
for round in range(rounds):
71+
Cinput = b''
72+
Cinput = Cinput + (P if round & 1 else C)
73+
if round % 3: Cinput = Cinput + S
74+
if round % 7: Cinput = Cinput + P
75+
Cinput = Cinput + (C if round & 1 else P)
76+
C = digest(Cinput)
77+
78+
if rounds == 5000:
79+
return '$6$' + salt.decode('ascii') + '$' + encode(C)
80+
else:
81+
return '$6$rounds=' + str(rounds) + '$' + salt.decode('ascii') + '$' + encode(C)
82+
83+
#---------------------------------------------------------------------------
84+
85+
def extract_salt_and_rounds(i): # i must be '$6$...'
86+
pieces = i.split('$')
87+
if pieces[1] != '6': raise TypeError('shacrypt512 only supports $6$ hashes')
88+
if pieces[2].startswith('rounds='):
89+
rounds = int(pieces[2][7:])
90+
if rounds < 1000: rounds = 1000
91+
if rounds > 999999999: rounds = 999999999
92+
return (pieces[3].encode('ascii'), rounds)
93+
else:
94+
return (pieces[2].encode('ascii'), 5000)
95+
96+
def password_ok(input_password, existing_crypted_password):
97+
(salt, rounds) = extract_salt_and_rounds(existing_crypted_password)
98+
return existing_crypted_password == shacrypt(input_password, salt, rounds)
99+
100+
if __name__ == '__main__':
101+
_test_password = 'Hello world!'.encode('ascii')
102+
_test_salt = 'saltstring'.encode('ascii')
103+
_test_rounds = 5000
104+
_test_crypted_password = '$6$saltstring$svn8UoSVapNtMuq1ukKS4tPQd8iKwSMHWjl/O817G3uBnIFNjnQJuesI68u4OTLiBFdcbYEdFCoEOfaS35inz1'
105+
assert shacrypt(_test_password, _test_salt, _test_rounds) == _test_crypted_password
106+
assert password_ok(_test_password, _test_crypted_password)
107+
108+
_test_password = 'Hello world!'.encode('ascii')
109+
_test_salt = 'saltstringsaltstring'.encode('ascii')
110+
_test_rounds = 10000
111+
_test_crypted_password = '$6$rounds=10000$saltstringsaltst$OW1/O6BYHV6BcXZu8QVeXbDWra3Oeqh0sbHbbMCVNSnCM/UrjmM0Dp8vOuZeHBy/YTBmSK6H9qs/y3RnOaw5v.'
112+
assert shacrypt(_test_password, _test_salt, _test_rounds) == _test_crypted_password
113+
assert password_ok(_test_password, _test_crypted_password)
114+
115+
import sys
116+
salt = None if len(sys.argv) < 2 else sys.argv[1].encode('ascii')
117+
print(shacrypt(sys.stdin.readline().strip().encode('utf-8'), salt))

web/src/p2k16/core/tool.py

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,23 @@ class DummyClient(object):
1414
pass
1515

1616

17+
def tool_event_to_json(event: Event):
18+
if type(event) is ToolCheckoutEvent:
19+
return {**model_to_json(event), **{
20+
"domain": event.domain,
21+
"name": event.name,
22+
"int1": event.int1,
23+
"created_at": event.created_at
24+
}}
25+
elif type(event) is ToolCheckinEvent:
26+
return {**model_to_json(event), **{
27+
"domain": event.domain,
28+
"name": event.name,
29+
"int1": event.int1,
30+
"created_at": event.created_at
31+
}}
32+
33+
1734
@event_management.converter_for("tool", "checkout")
1835
class ToolCheckoutEvent(object):
1936
def __init__(self, tool_name: str, created_at: Optional[datetime] = None, created_by: Optional[Account] = None):
@@ -31,7 +48,7 @@ def from_event(event: Event) -> "ToolCheckoutEvent":
3148
def to_dict(self):
3249
return {**event_management.base_dict(self), **{
3350
"created_at": self.created_at,
34-
"created_by": self.created_by,
51+
"created_by": self.created_by.id,
3552
"created_by_username": self.created_by.username,
3653
"tool_name": self.tool_name
3754
}}
@@ -54,7 +71,7 @@ def from_event(event: Event) -> "ToolCheckinEvent":
5471
def to_dict(self):
5572
return {**event_management.base_dict(self), **{
5673
"created_at": self.created_at,
57-
"created_by": self.created_by,
74+
"created_by": self.created_by.id,
5875
"created_by_username": self.created_by.username,
5976
"tool_name": self.tool_name
6077
}}

web/src/p2k16/web/server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import flask_bower
77
import flask_login
88
import werkzeug.exceptions
9-
from flask.json import JSONEncoder
9+
from json import JSONEncoder
1010
from p2k16.core import P2k16UserException, P2k16TechnicalException, membership_management
1111
from p2k16.core import make_app, auth, door, mail, tool, label
1212
from p2k16.core.log import P2k16LoggingFilter

web/src/p2k16/web/tool_blueprint.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def tool_to_json(tool: ToolDescription):
6262
checkout_model = {
6363
"active": True,
6464
"started": checkout.started,
65-
"account": checkout.account,
65+
"account": checkout.account.id,
6666
"username": checkout.account.username,
6767
}
6868
else:
@@ -72,7 +72,7 @@ def tool_to_json(tool: ToolDescription):
7272

7373

7474
return {**model_to_json(tool), **{
75-
"name": tool.name,
75+
"name": tool.name,
7676
"description": tool.description,
7777
"circle": tool.circle.name,
7878
"checkout": checkout_model,

0 commit comments

Comments
 (0)