62 lines
2.4 KiB
Python
62 lines
2.4 KiB
Python
"""Bounded HTTPS JSON requests. Never return provider errors or credentials."""
|
|
import json
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
|
|
|
|
class NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
def redirect_request(self, *args):
|
|
return None
|
|
|
|
|
|
def opener():
|
|
return urllib.request.build_opener(urllib.request.ProxyHandler({}), NoRedirect())
|
|
|
|
|
|
def read_limited(response, limit, deadline):
|
|
chunks = []
|
|
total = 0
|
|
while total <= limit:
|
|
if time.monotonic() >= deadline:
|
|
raise TimeoutError()
|
|
# read1 returns after one underlying read instead of filling a huge buffer.
|
|
read = getattr(response, 'read1', response.read)
|
|
block = read(min(65536, limit + 1 - total))
|
|
if not block:
|
|
return b''.join(chunks)
|
|
total += len(block)
|
|
chunks.append(block)
|
|
raise ValueError('response_too_large')
|
|
|
|
|
|
def request_json(url, *, payload=None, headers=None, max_bytes=65536):
|
|
if not isinstance(max_bytes,int) or not 1<=max_bytes<=524288:
|
|
raise ValueError('Invalid JSON response limit')
|
|
from .worker import enabled,invoke
|
|
if enabled():
|
|
result=invoke('json',{'url':url,'payload':payload,'headers':headers,'max_bytes':max_bytes})
|
|
return tuple(result) if isinstance(result,list) and len(result)==2 else (None,'request_failed')
|
|
request_headers = {'Accept': 'application/json', 'Accept-Encoding': 'identity',
|
|
'User-Agent': 'VPSMaster/1.0.0b1'}
|
|
request_headers.update(headers or {})
|
|
data = None
|
|
if payload is not None:
|
|
data = json.dumps(payload).encode()
|
|
request_headers['Content-Type'] = 'application/json'
|
|
request = urllib.request.Request(url, data=data, headers=request_headers)
|
|
deadline = time.monotonic() + 15
|
|
try:
|
|
with opener().open(request, timeout=8) as response:
|
|
if response.status != 200:
|
|
return None, 'http_' + str(response.status)
|
|
result = json.loads(read_limited(response, max_bytes, deadline))
|
|
if not isinstance(result, dict):
|
|
return None, 'invalid_response'
|
|
return result, 'ok'
|
|
except urllib.error.HTTPError as exc:
|
|
exc.close()
|
|
return None, {401: 'invalid_key', 403: 'denied', 429: 'rate_limited'}.get(exc.code, 'http_' + str(exc.code))
|
|
except (ValueError, OSError, urllib.error.URLError):
|
|
return None, 'request_failed'
|