diff --git a/IzzyLib/http.py b/IzzyLib/http.py index 4f915d9..d320573 100644 --- a/IzzyLib/http.py +++ b/IzzyLib/http.py @@ -12,6 +12,13 @@ from urllib.request import Request, urlopen from . import error, __version__ + +try: + import requests +except ImportError: + logging.verbose('Requests module not found. RequestsClient disabled') + requests = False + try: from Crypto.Hash import SHA256 from Crypto.PublicKey import RSA @@ -35,6 +42,7 @@ except ImportError: Client = None +methods = ['connect', 'delete', 'get', 'head', 'options', 'patch', 'post', 'put', 'trace'] class HttpClient(object): @@ -107,7 +115,7 @@ class HttpClient(object): request = Request(url, data=data, headers=parsed_headers, method=method) if self.proxy.enabled: - request.set_proxy(f'{self.proxy.host}:{self.proxy.host}', self.proxy.ptype) + request.set_proxy(f'{self.proxy.host}:{self.proxy.port}', self.proxy.ptype) return request @@ -223,6 +231,212 @@ class HttpResponse(object): return json.dumps(self.json().asDict(), indent=indent) +class RequestsClient(object): + def __init__(self, headers={}, useragent=f'IzzyLib/{__version__}', appagent=None, proxy_type='https', proxy_host=None, proxy_port=None): + proxy_ports = { + 'http': 80, + 'https': 443 + } + + if proxy_type not in ['http', 'https']: + raise ValueError(f'Not a valid proxy type: {proxy_type}') + + self.headers=headers + self.agent = f'{useragent} ({appagent})' if appagent else useragent + self.proxy = DotDict({ + 'enabled': True if proxy_host else False, + 'ptype': proxy_type, + 'host': proxy_host, + 'port': proxy_ports[proxy_type] if not proxy_port else proxy_port + }) + + self.SetGlobal = SetClient + + + def __sign_request(self, request, privkey, keyid): + if not crypto_enabled: + logging.error('Crypto functions disabled') + return + + request.add_header('(request-target)', f'{request.method.lower()} {request.path}') + request.add_header('host', request.host) + request.add_header('date', datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')) + + if request.body: + body_hash = b64encode(SHA256.new(request.body).digest()).decode("UTF-8") + request.add_header('digest', f'SHA-256={body_hash}') + request.add_header('content-length', len(request.body)) + + sig = { + 'keyId': keyid, + 'algorithm': 'rsa-sha256', + 'headers': ' '.join([k.lower() for k in request.headers.keys()]), + 'signature': b64encode(PkcsHeaders(privkey, request.headers)).decode('UTF-8') + } + + sig_items = [f'{k}="{v}"' for k,v in sig.items()] + sig_string = ','.join(sig_items) + + request.add_header('signature', sig_string) + + request.remove_header('(request-target)') + request.remove_header('host') + + + def request(self, *args, method='get', **kwargs): + if method.lower() not in methods: + raise ValueError(f'Invalid method: {method}') + + request = RequestsRequest(self, *args, method=method.lower(), **kwargs) + return RequestsResponse(request.send()) + + + def file(self, url, filepath, *args, filename=None, **kwargs): + resp = self.request(url, *args, **kwargs) + + if resp.status != 200: + logging.error(f'Failed to download {url}:', resp.status, resp.body) + return False + + return resp.save(filepath) + + + def image(self, url, filepath, *args, filename=None, ext='png', dimensions=(50, 50), **kwargs): + if not Image: + logging.error('Pillow module is not installed') + return + + resp = self.request(url, *args, **kwargs) + + if resp.status != 200: + logging.error(f'Failed to download {url}:', resp.status, resp.body) + return False + + if not filename: + filename = Path(url).stem() + + path = Path(filepath) + + if not path.exists(): + logging.error('Path does not exist:', path) + return False + + byte = BytesIO() + image = Image.open(BytesIO(resp.body)) + image.thumbnail(dimensions) + image.save(byte, format=ext.upper()) + + with path.join(filename).open('wb') as fd: + fd.write(byte.getvalue()) + + + def json(self, *args, **kwargs): + return self.dict(*args, **kwargs) + + + def dict(self, *args, headers={}, activity=True, **kwargs): + json_type = 'activity+json' if activity else 'json' + headers.update({ + 'accept': f'application/{json_type}' + }) + return self.request(*args, headers=headers, **kwargs).dict + + + def signed_request(self, privkey, keyid, *args, **kwargs): + request = RequestsRequest(self, *args, **kwargs) + self.__sign_request(request, privkey, keyid) + return RequestsResponse(request.send()) + + +class RequestsRequest(object): + def __init__(self, client, url, data=None, headers={}, query={}, method='get'): + self.args = [url] + self.kwargs = {'params': query} + self.method = method.lower() + self.client = client + + new_headers = client.headers.copy() + new_headers.update(headers) + + parsed_headers = {k.lower(): v for k,v in new_headers.items()} + + if not parsed_headers.get('user-agent'): + parsed_headers['user-agent'] = client.agent + + self.kwargs['headers'] = new_headers + self.kwargs['data'] = data + + if client.proxy.enabled: + self.kwargs['proxies'] = {self.proxy.ptype: f'{self.proxy.ptype}://{self.proxy.host}:{self.proxy.port}'} + + + def send(self): + func = getattr(requests, self.method) + return func(*self.args, **self.kwargs) + + +class RequestsResponse(object): + def __init__(self, response): + self.response = response + self.data = b'' + self.headers = DefaultDict({k.lower(): v.lower() for k,v in response.headers.items()}) + self.status = response.status_code + self.url = response.url + + + def chunks(self, size=256): + return self.response.iter_content(chunk_size=256) + + + @property + def body(self): + for chunk in self.chunks(): + self.data += chunk + + return self.data + + + @property + def text(self): + if not self.data: + return self.body.decode(self.response.encoding) + + return self.data.decode(self.response.encoding) + + + @property + def dict(self): + try: + return DotDict(self.text) + except Exception as e: + return DotDict() + + + @property + def json(self): + return json.dumps(self.dict) + + + @property + def json_pretty(self, indent=4): + return json.dumps(self.dict, indent=indent) + + + def save(self, path, overwrite=True): + path = Path(path) + parent = path.parent() + + if not parent.exists(): + raise ValueError(f'Path does not exist: {parent}') + + if overwrite and path.exists(): + path.delete() + + with path.open('wb') as fd: + for chunk in self.chunks(): + fd.write(chunk) + + def VerifyRequest(request: SanicRequest, actor: dict): '''Verify a header signature from a sanic request