-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathauth.py
More file actions
129 lines (109 loc) · 4.95 KB
/
Copy pathauth.py
File metadata and controls
129 lines (109 loc) · 4.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
from dataclasses import dataclass
import hashlib
import hmac
import time
from httpx import Request, QueryParams, AsyncClient
from urllib.parse import urlparse, urlunparse, unquote
from typing import Optional, TypeVar
from bs4 import BeautifulSoup, Tag
from collections.abc import Iterable
import logging
logger = logging.getLogger(__name__)
SignType = TypeVar("SignType", bound=Request)
class Auth:
def sign(self, request: SignType, client: AsyncClient) -> SignType:
raise Exception("No authentication was provided")
def url_without_scheme(url: str) -> str:
"""
Returns the URL in the appropriate format for the signature calculation, namely:
• Without a scheme
• Without URL encoding
• With query parameters
"""
return unquote(urlunparse(urlparse(url)._replace(scheme="")).lstrip("/"))
@dataclass
class UserAuth(Auth):
"""
Used to authenticate the `FileSenderClient` with all permissions of a full user.
Attributes:
username: The username (generally the email address) of the user performing FileSender operations
api_key: The API key that corresponds to the username. You can generally obtain this at the <https://some.filesender.domain/?s=user> URL.
delay: The number of seconds to delay the timestamp. See <https://docs.filesender.org/filesender/v2.0/rest/#signed-request>
"""
username: str
api_key: str
delay: int = 0
def sign(self, request: SignType, client: AsyncClient) -> SignType:
# Merge in some additional parameters, and then sort by key
# so the params are in alphabetical order as required
params = QueryParams(
tuple(
sorted(
request.url.params.merge(
{
"remote_user": self.username,
"timestamp": str(round(time.time() + self.delay)),
# Manually add the session params so we can force them to be
# alphabetical order
# **cast(Dict[str, str], session.params),
# **request.params
}
).items()
)
)
)
request.url = request.url.copy_with(params=params)
signature = hmac.new(key=self.api_key.encode(), digestmod=hashlib.sha1)
signature.update(request.method.lower().encode())
signature.update(b"&")
signature.update(url_without_scheme(str(request.url)).encode())
if isinstance(request.stream, Iterable):
for i, chunk in enumerate(request.stream):
if i == 0:
signature.update(b"&")
signature.update(chunk)
else:
raise Exception("?")
request.url = request.url.copy_remove_param("signature").copy_add_param(
"signature", signature.hexdigest()
)
return request
@dataclass(unsafe_hash=True)
class GuestAuth(Auth):
"""
Used to authenticate the FileSenderClient with a guest token
Attributes:
guest_token: The string after `vid=` in the voucher link
"""
guest_token: str
security_token: Optional[str] = None
# The CSRF token is configurable per-server, so we need to store it if the server provides it, but it isn't mandatory
# See https://github.com/filesender/filesender/issues/2732#issuecomment-4609996918
csrf_token: Optional[str] = None
async def prepare(self, client: AsyncClient):
res = await client.get(
"https://filesender.aarnet.edu.au",
params={"s": "upload", "vid": self.guest_token},
)
soup = BeautifulSoup(res.content, "html.parser")
body = soup.find("body")
if not isinstance(body, Tag):
raise Exception("Invalid HTML document")
self.security_token = body.attrs["data-security-token"]
self.csrf_token = res.cookies.get("csrfptoken")
# We might already have the token, because we requested the server info earlier
if self.csrf_token is None and "csrfptoken" in client.cookies:
for cookie in client.cookies.jar:
if cookie.name.lower() == "csrfptoken":
self.csrf_token = cookie.value
def sign(self, request: SignType, client: AsyncClient) -> SignType:
request.url = request.url.copy_add_param("vid", self.guest_token)
if self.security_token is None:
raise Exception(
".prepare() must be called on the GuestAuth before it is used to sign requests"
)
request.headers["X-Filesender-Security-Token"] = self.security_token
if self.csrf_token is not None:
# If we have a CSRF token, the server requires it so we should use it
request.headers["csrfptoken"] = self.csrf_token
return request