Skip to content

Commit 2cfd6da

Browse files
committed
Make the csrf token optional
1 parent 1a52410 commit 2cfd6da

1 file changed

Lines changed: 39 additions & 25 deletions

File tree

filesender/auth.py

Lines changed: 39 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
logger = logging.getLogger(__name__)
1313

1414
SignType = TypeVar("SignType", bound=Request)
15+
16+
1517
class Auth:
1618
def sign(self, request: SignType, client: AsyncClient) -> SignType:
1719
raise Exception("No authentication was provided")
@@ -26,37 +28,44 @@ def url_without_scheme(url: str) -> str:
2628
"""
2729
return unquote(urlunparse(urlparse(url)._replace(scheme="")).lstrip("/"))
2830

31+
2932
@dataclass
3033
class UserAuth(Auth):
3134
"""
3235
Used to authenticate the `FileSenderClient` with all permissions of a full user.
3336
3437
Attributes:
35-
username: The username (generally the email address) of the user performing FileSender operations
38+
username: The username (generally the email address) of the user performing FileSender operations
3639
api_key: The API key that corresponds to the username. You can generally obtain this at the <https://some.filesender.domain/?s=user> URL.
3740
delay: The number of seconds to delay the timestamp. See <https://docs.filesender.org/filesender/v2.0/rest/#signed-request>
3841
"""
42+
3943
username: str
4044
api_key: str
4145
delay: int = 0
4246

4347
def sign(self, request: SignType, client: AsyncClient) -> SignType:
4448
# Merge in some additional parameters, and then sort by key
4549
# so the params are in alphabetical order as required
46-
params = QueryParams(tuple(sorted(request.url.params.merge({
47-
"remote_user": self.username,
48-
"timestamp": str(round(time.time() + self.delay)),
49-
# Manually add the session params so we can force them to be
50-
# alphabetical order
51-
# **cast(Dict[str, str], session.params),
52-
# **request.params
53-
}).items())))
54-
request.url = request.url.copy_with(params=params)
55-
56-
signature = hmac.new(
57-
key=self.api_key.encode(),
58-
digestmod=hashlib.sha1
50+
params = QueryParams(
51+
tuple(
52+
sorted(
53+
request.url.params.merge(
54+
{
55+
"remote_user": self.username,
56+
"timestamp": str(round(time.time() + self.delay)),
57+
# Manually add the session params so we can force them to be
58+
# alphabetical order
59+
# **cast(Dict[str, str], session.params),
60+
# **request.params
61+
}
62+
).items()
63+
)
64+
)
5965
)
66+
request.url = request.url.copy_with(params=params)
67+
68+
signature = hmac.new(key=self.api_key.encode(), digestmod=hashlib.sha1)
6069
signature.update(request.method.lower().encode())
6170
signature.update(b"&")
6271
signature.update(url_without_scheme(str(request.url)).encode())
@@ -69,9 +78,12 @@ def sign(self, request: SignType, client: AsyncClient) -> SignType:
6978
else:
7079
raise Exception("?")
7180

72-
request.url = request.url.copy_remove_param("signature").copy_add_param("signature", signature.hexdigest())
81+
request.url = request.url.copy_remove_param("signature").copy_add_param(
82+
"signature", signature.hexdigest()
83+
)
7384
return request
7485

86+
7587
@dataclass(unsafe_hash=True)
7688
class GuestAuth(Auth):
7789
"""
@@ -80,19 +92,19 @@ class GuestAuth(Auth):
8092
Attributes:
8193
guest_token: The string after `vid=` in the voucher link
8294
"""
95+
8396
guest_token: str
8497
security_token: Optional[str] = None
98+
# The CSRF token is configurable per-server, so we need to store it if the server provides it, but it isn't mandatory
99+
# See https://github.com/filesender/filesender/issues/2732#issuecomment-4609996918
85100
csrf_token: Optional[str] = None
86101

87102
async def prepare(self, client: AsyncClient):
88103
res = await client.get(
89104
"https://filesender.aarnet.edu.au",
90-
params={
91-
"s": "upload",
92-
"vid": self.guest_token
93-
}
105+
params={"s": "upload", "vid": self.guest_token},
94106
)
95-
soup = BeautifulSoup(res.content, 'html.parser')
107+
soup = BeautifulSoup(res.content, "html.parser")
96108
body = soup.find("body")
97109
if not isinstance(body, Tag):
98110
raise Exception("Invalid HTML document")
@@ -103,13 +115,15 @@ async def prepare(self, client: AsyncClient):
103115
for cookie in client.cookies.jar:
104116
if cookie.name.lower() == "csrfptoken":
105117
self.csrf_token = cookie.value
106-
if self.csrf_token is None:
107-
logger.warning("No CSRF token could be found!")
108118

109119
def sign(self, request: SignType, client: AsyncClient) -> SignType:
110120
request.url = request.url.copy_add_param("vid", self.guest_token)
111-
if self.security_token is None or self.csrf_token is None:
112-
raise Exception(".prepare() must be called on the GuestAuth before it is used to sign requests")
121+
if self.security_token is None:
122+
raise Exception(
123+
".prepare() must be called on the GuestAuth before it is used to sign requests"
124+
)
113125
request.headers["X-Filesender-Security-Token"] = self.security_token
114-
request.headers["csrfptoken"] = self.csrf_token
126+
if self.csrf_token is not None:
127+
# If we have a CSRF token, the server requires it so we should use it
128+
request.headers["csrfptoken"] = self.csrf_token
115129
return request

0 commit comments

Comments
 (0)