88import json
99from types import MappingProxyType
1010from datetime import datetime , timedelta , timezone
11+ from urllib .parse import urlparse
1112
1213import aiohttp
1314from requests .structures import CaseInsensitiveDict
1819TIMEOUT = 60
1920
2021
22+ def _is_transloadit_host (hostname ):
23+ return hostname == "transloadit.com" or hostname .endswith (".transloadit.com" )
24+
25+
2126def _get_upload_filename (file_stream , fallback ):
2227 name = getattr (file_stream , "name" , None )
2328 if isinstance (name , (bytes , os .PathLike )):
@@ -60,6 +65,12 @@ def seek(self, *args):
6065 return self ._file_stream .seek (* args )
6166
6267 def seekable (self ):
68+ seekable = getattr (self ._file_stream , "seekable" , None )
69+ if callable (seekable ):
70+ try :
71+ return seekable ()
72+ except (OSError , ValueError ):
73+ return False
6374 return hasattr (self ._file_stream , "seek" )
6475
6576 def tell (self ):
@@ -121,14 +132,18 @@ def _timeout(self, files=False):
121132 )
122133
123134 def _normalize_payload (self , data ):
124- normalized = {}
135+ normalized = []
125136 for key , value in data .items ():
126137 if value is None :
127138 continue
128- if isinstance (value , bool ):
129- normalized [key ] = "true" if value else "false"
130- else :
131- normalized [key ] = str (value )
139+ values = value if isinstance (value , (list , tuple )) else [value ]
140+ for item in values :
141+ if item is None :
142+ continue
143+ if isinstance (item , bool ):
144+ normalized .append ((key , "true" if item else "false" ))
145+ else :
146+ normalized .append ((key , str (item )))
132147 return normalized
133148
134149 async def _read_response_data (self , response ):
@@ -144,9 +159,10 @@ async def get(self, path, params=None):
144159 """
145160 Makes an asynchronous HTTP GET request.
146161 """
162+ url = self ._get_full_url (path )
147163 session = await self ._ensure_session ()
148164 async with session .get (
149- self . _get_full_url ( path ) ,
165+ url ,
150166 params = self ._to_payload (params ),
151167 headers = self ._headers (),
152168 timeout = self ._timeout (),
@@ -161,14 +177,15 @@ async def post(self, path, data=None, extra_data=None, files=None):
161177 """
162178 Makes an asynchronous HTTP POST request.
163179 """
180+ url = self ._get_full_url (path )
164181 session = await self ._ensure_session ()
165182 data = self ._to_payload (data )
166183 if extra_data :
167184 data .update (extra_data )
168185
169186 if files :
170187 form = aiohttp .FormData ()
171- for key , value in self ._normalize_payload (data ). items () :
188+ for key , value in self ._normalize_payload (data ):
172189 form .add_field (key , value )
173190
174191 for key , file_stream in files .items ():
@@ -185,7 +202,7 @@ async def post(self, path, data=None, extra_data=None, files=None):
185202 payload = self ._normalize_payload (data )
186203
187204 async with session .post (
188- self . _get_full_url ( path ) ,
205+ url ,
189206 data = payload ,
190207 headers = self ._headers (),
191208 timeout = self ._timeout (files = bool (files )),
@@ -200,10 +217,11 @@ async def put(self, path, data=None):
200217 """
201218 Makes an asynchronous HTTP PUT request.
202219 """
220+ url = self ._get_full_url (path )
203221 session = await self ._ensure_session ()
204222 data = self ._normalize_payload (self ._to_payload (data ))
205223 async with session .put (
206- self . _get_full_url ( path ) ,
224+ url ,
207225 data = data ,
208226 headers = self ._headers (),
209227 timeout = self ._timeout (),
@@ -218,10 +236,11 @@ async def delete(self, path, data=None):
218236 """
219237 Makes an asynchronous HTTP DELETE request.
220238 """
239+ url = self ._get_full_url (path )
221240 session = await self ._ensure_session ()
222241 data = self ._normalize_payload (self ._to_payload (data ))
223242 async with session .delete (
224- self . _get_full_url ( path ) ,
243+ url ,
225244 data = data ,
226245 headers = self ._headers (),
227246 timeout = self ._timeout (),
@@ -252,5 +271,15 @@ def _sign_data(self, message):
252271
253272 def _get_full_url (self , url ):
254273 if url .startswith (("http://" , "https://" )):
274+ service = urlparse (self .transloadit .service )
275+ target = urlparse (url )
276+ same_origin = (target .scheme , target .netloc ) == (service .scheme , service .netloc )
277+ transloadit_origin = (
278+ target .scheme == service .scheme
279+ and _is_transloadit_host (service .hostname or "" )
280+ and _is_transloadit_host (target .hostname or "" )
281+ )
282+ if not (same_origin or transloadit_origin ):
283+ raise ValueError ("Absolute API URLs must use the configured Transloadit service origin." )
255284 return url
256285 return self .transloadit .service + url
0 commit comments