1212logger = logging .getLogger (__name__ )
1313
1414SignType = TypeVar ("SignType" , bound = Request )
15+
16+
1517class 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
3033class 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 )
7688class 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