epicyon/session.py

566 lines
21 KiB
Python

__filename__ = "session.py"
__author__ = "Bob Mottram"
__license__ = "AGPL3+"
__version__ = "1.2.0"
__maintainer__ = "Bob Mottram"
__email__ = "bob@libreserver.org"
__status__ = "Production"
__module_group__ = "Session"
import os
import requests
from utils import url_permitted
from utils import is_image_file
from httpsig import create_signed_header
import json
from socket import error as SocketError
import errno
from http.client import HTTPConnection
base_directory = None
def create_session(proxy_type: str):
session = None
try:
session = requests.session()
except requests.exceptions.RequestException as ex:
print('WARN: requests error during create_session ' + str(ex))
return None
except SocketError as ex:
if ex.errno == errno.ECONNRESET:
print('WARN: connection was reset during create_session ' +
str(ex))
else:
print('WARN: socket error during create_session ' + str(ex))
return None
except ValueError as ex:
print('WARN: error during create_session ' + str(ex))
return None
if not session:
return None
if proxy_type == 'tor':
session.proxies = {}
session.proxies['http'] = 'socks5h://localhost:9050'
session.proxies['https'] = 'socks5h://localhost:9050'
elif proxy_type == 'i2p':
session.proxies = {}
session.proxies['http'] = 'socks5h://localhost:4447'
session.proxies['https'] = 'socks5h://localhost:4447'
elif proxy_type == 'gnunet':
session.proxies = {}
session.proxies['http'] = 'socks5h://localhost:7777'
session.proxies['https'] = 'socks5h://localhost:7777'
# print('New session created with proxy ' + str(proxy_type))
return session
def url_exists(session, url: str, timeoutSec: int = 3,
http_prefix: str = 'https', domain: str = 'testdomain') -> bool:
if not isinstance(url, str):
print('url: ' + str(url))
print('ERROR: url_exists failed, url should be a string')
return False
sessionParams = {}
sessionHeaders = {}
sessionHeaders['User-Agent'] = 'Epicyon/' + __version__
if domain:
sessionHeaders['User-Agent'] += \
'; +' + http_prefix + '://' + domain + '/'
if not session:
print('WARN: url_exists failed, no session specified')
return True
try:
result = session.get(url, headers=sessionHeaders,
params=sessionParams,
timeout=timeoutSec)
if result:
if result.status_code == 200 or \
result.status_code == 304:
return True
else:
print('url_exists for ' + url + ' returned ' +
str(result.status_code))
except BaseException:
print('EX: url_exists GET failed ' + str(url))
pass
return False
def _get_json_request(session, url: str, domain_full: str, sessionHeaders: {},
sessionParams: {}, timeoutSec: int,
signing_priv_key_pem: str, quiet: bool, debug: bool,
returnJson: bool) -> {}:
"""http GET for json
"""
try:
result = session.get(url, headers=sessionHeaders,
params=sessionParams, timeout=timeoutSec)
if result.status_code != 200:
if result.status_code == 401:
print("WARN: get_json " + url + ' rejected by secure mode')
elif result.status_code == 403:
print('WARN: get_json Forbidden url: ' + url)
elif result.status_code == 404:
print('WARN: get_json Not Found url: ' + url)
elif result.status_code == 410:
print('WARN: get_json no longer available url: ' + url)
else:
print('WARN: get_json url: ' + url +
' failed with error code ' +
str(result.status_code) +
' headers: ' + str(sessionHeaders))
if returnJson:
return result.json()
return result.content
except requests.exceptions.RequestException as ex:
sessionHeaders2 = sessionHeaders.copy()
if sessionHeaders2.get('Authorization'):
sessionHeaders2['Authorization'] = 'REDACTED'
if debug and not quiet:
print('ERROR: get_json failed, url: ' + str(url) + ', ' +
'headers: ' + str(sessionHeaders2) + ', ' +
'params: ' + str(sessionParams) + ', ' + str(ex))
except ValueError as ex:
sessionHeaders2 = sessionHeaders.copy()
if sessionHeaders2.get('Authorization'):
sessionHeaders2['Authorization'] = 'REDACTED'
if debug and not quiet:
print('ERROR: get_json failed, url: ' + str(url) + ', ' +
'headers: ' + str(sessionHeaders2) + ', ' +
'params: ' + str(sessionParams) + ', ' + str(ex))
except SocketError as ex:
if not quiet:
if ex.errno == errno.ECONNRESET:
print('WARN: get_json failed, ' +
'connection was reset during get_json ' + str(ex))
return None
def _get_json_signed(session, url: str, domain_full: str, sessionHeaders: {},
sessionParams: {}, timeoutSec: int,
signing_priv_key_pem: str, quiet: bool,
debug: bool) -> {}:
"""Authorized fetch - a signed version of GET
"""
if not domain_full:
if debug:
print('No sending domain for signed GET')
return None
if '://' not in url:
print('Invalid url: ' + url)
return None
http_prefix = url.split('://')[0]
toDomainFull = url.split('://')[1]
if '/' in toDomainFull:
toDomainFull = toDomainFull.split('/')[0]
if ':' in domain_full:
domain = domain_full.split(':')[0]
port = domain_full.split(':')[1]
else:
domain = domain_full
if http_prefix == 'https':
port = 443
else:
port = 80
if ':' in toDomainFull:
toDomain = toDomainFull.split(':')[0]
toPort = toDomainFull.split(':')[1]
else:
toDomain = toDomainFull
if http_prefix == 'https':
toPort = 443
else:
toPort = 80
if debug:
print('Signed GET domain: ' + domain + ' ' + str(port))
print('Signed GET toDomain: ' + toDomain + ' ' + str(toPort))
print('Signed GET url: ' + url)
print('Signed GET http_prefix: ' + http_prefix)
messageStr = ''
withDigest = False
if toDomainFull + '/' in url:
path = '/' + url.split(toDomainFull + '/')[1]
else:
path = '/actor'
content_type = 'application/activity+json'
if sessionHeaders.get('Accept'):
content_type = sessionHeaders['Accept']
signatureHeaderJson = \
create_signed_header(None, signing_priv_key_pem, 'actor', domain, port,
toDomain, toPort, path, http_prefix, withDigest,
messageStr, content_type)
if debug:
print('Signed GET signatureHeaderJson ' + str(signatureHeaderJson))
# update the session headers from the signature headers
sessionHeaders['Host'] = signatureHeaderJson['host']
sessionHeaders['Date'] = signatureHeaderJson['date']
sessionHeaders['Accept'] = signatureHeaderJson['accept']
sessionHeaders['Signature'] = signatureHeaderJson['signature']
sessionHeaders['Content-Length'] = '0'
if debug:
print('Signed GET sessionHeaders ' + str(sessionHeaders))
returnJson = True
if 'json' not in content_type:
returnJson = False
return _get_json_request(session, url, domain_full, sessionHeaders,
sessionParams, timeoutSec, None, quiet,
debug, returnJson)
def get_json(signing_priv_key_pem: str,
session, url: str, headers: {}, params: {}, debug: bool,
version: str = '1.2.0', http_prefix: str = 'https',
domain: str = 'testdomain',
timeoutSec: int = 20, quiet: bool = False) -> {}:
if not isinstance(url, str):
if debug and not quiet:
print('url: ' + str(url))
print('ERROR: get_json failed, url should be a string')
return None
sessionParams = {}
sessionHeaders = {}
if headers:
sessionHeaders = headers
if params:
sessionParams = params
sessionHeaders['User-Agent'] = 'Epicyon/' + version
if domain:
sessionHeaders['User-Agent'] += \
'; +' + http_prefix + '://' + domain + '/'
if not session:
if not quiet:
print('WARN: get_json failed, no session specified for get_json')
return None
if debug:
HTTPConnection.debuglevel = 1
if signing_priv_key_pem:
return _get_json_signed(session, url, domain,
sessionHeaders, sessionParams,
timeoutSec, signing_priv_key_pem,
quiet, debug)
else:
return _get_json_request(session, url, domain, sessionHeaders,
sessionParams, timeoutSec,
None, quiet, debug, True)
def download_html(signing_priv_key_pem: str,
session, url: str, headers: {}, params: {}, debug: bool,
version: str = '1.2.0', http_prefix: str = 'https',
domain: str = 'testdomain',
timeoutSec: int = 20, quiet: bool = False) -> {}:
if not isinstance(url, str):
if debug and not quiet:
print('url: ' + str(url))
print('ERROR: download_html failed, url should be a string')
return None
sessionParams = {}
sessionHeaders = {}
if headers:
sessionHeaders = headers
if params:
sessionParams = params
sessionHeaders['Accept'] = 'text/html'
sessionHeaders['User-Agent'] = 'Epicyon/' + version
if domain:
sessionHeaders['User-Agent'] += \
'; +' + http_prefix + '://' + domain + '/'
if not session:
if not quiet:
print('WARN: download_html failed, ' +
'no session specified for download_html')
return None
if debug:
HTTPConnection.debuglevel = 1
if signing_priv_key_pem:
return _get_json_signed(session, url, domain,
sessionHeaders, sessionParams,
timeoutSec, signing_priv_key_pem,
quiet, debug)
else:
return _get_json_request(session, url, domain, sessionHeaders,
sessionParams, timeoutSec,
None, quiet, debug, False)
def post_json(http_prefix: str, domain_full: str,
session, post_json_object: {}, federation_list: [],
inboxUrl: str, headers: {}, timeoutSec: int = 60,
quiet: bool = False) -> str:
"""Post a json message to the inbox of another person
"""
# check that we are posting to a permitted domain
if not url_permitted(inboxUrl, federation_list):
if not quiet:
print('post_json: ' + inboxUrl + ' not permitted')
return None
sessionHeaders = headers
sessionHeaders['User-Agent'] = 'Epicyon/' + __version__
sessionHeaders['User-Agent'] += \
'; +' + http_prefix + '://' + domain_full + '/'
try:
postResult = \
session.post(url=inboxUrl,
data=json.dumps(post_json_object),
headers=headers, timeout=timeoutSec)
except requests.Timeout as ex:
if not quiet:
print('ERROR: post_json timeout ' + inboxUrl + ' ' +
json.dumps(post_json_object) + ' ' + str(headers))
print(ex)
return ''
except requests.exceptions.RequestException as ex:
if not quiet:
print('ERROR: post_json requests failed ' + inboxUrl + ' ' +
json.dumps(post_json_object) + ' ' + str(headers) +
' ' + str(ex))
return None
except SocketError as ex:
if not quiet and ex.errno == errno.ECONNRESET:
print('WARN: connection was reset during post_json')
return None
except ValueError as ex:
if not quiet:
print('ERROR: post_json failed ' + inboxUrl + ' ' +
json.dumps(post_json_object) + ' ' + str(headers) +
' ' + str(ex))
return None
if postResult:
return postResult.text
return None
def post_json_string(session, post_jsonStr: str,
federation_list: [],
inboxUrl: str,
headers: {},
debug: bool,
timeoutSec: int = 30,
quiet: bool = False) -> (bool, bool, int):
"""Post a json message string to the inbox of another person
The second boolean returned is true if the send is unauthorized
NOTE: Here we post a string rather than the original json so that
conversions between string and json format don't invalidate
the message body digest of http signatures
"""
try:
postResult = \
session.post(url=inboxUrl, data=post_jsonStr,
headers=headers, timeout=timeoutSec)
except requests.exceptions.RequestException as ex:
if not quiet:
print('WARN: error during post_json_string requests ' + str(ex))
return None, None, 0
except SocketError as ex:
if not quiet and ex.errno == errno.ECONNRESET:
print('WARN: connection was reset during post_json_string')
if not quiet:
print('ERROR: post_json_string failed ' + inboxUrl + ' ' +
post_jsonStr + ' ' + str(headers))
return None, None, 0
except ValueError as ex:
if not quiet:
print('WARN: error during post_json_string ' + str(ex))
return None, None, 0
if postResult.status_code < 200 or postResult.status_code > 202:
if postResult.status_code >= 400 and \
postResult.status_code <= 405 and \
postResult.status_code != 404:
if not quiet:
print('WARN: Post to ' + inboxUrl +
' is unauthorized. Code ' +
str(postResult.status_code))
return False, True, postResult.status_code
else:
if not quiet:
print('WARN: Failed to post to ' + inboxUrl +
' with headers ' + str(headers) +
' status code ' + str(postResult.status_code))
return False, False, postResult.status_code
return True, False, 0
def post_image(session, attachImageFilename: str, federation_list: [],
inboxUrl: str, headers: {}) -> str:
"""Post an image to the inbox of another person or outbox via c2s
"""
# check that we are posting to a permitted domain
if not url_permitted(inboxUrl, federation_list):
print('post_json: ' + inboxUrl + ' not permitted')
return None
if not is_image_file(attachImageFilename):
print('Image must be png, jpg, webp, avif, gif or svg')
return None
if not os.path.isfile(attachImageFilename):
print('Image not found: ' + attachImageFilename)
return None
content_type = 'image/jpeg'
if attachImageFilename.endswith('.png'):
content_type = 'image/png'
elif attachImageFilename.endswith('.gif'):
content_type = 'image/gif'
elif attachImageFilename.endswith('.webp'):
content_type = 'image/webp'
elif attachImageFilename.endswith('.avif'):
content_type = 'image/avif'
elif attachImageFilename.endswith('.svg'):
content_type = 'image/svg+xml'
headers['Content-type'] = content_type
with open(attachImageFilename, 'rb') as avFile:
mediaBinary = avFile.read()
try:
postResult = session.post(url=inboxUrl, data=mediaBinary,
headers=headers)
except requests.exceptions.RequestException as ex:
print('WARN: error during post_image requests ' + str(ex))
return None
except SocketError as ex:
if ex.errno == errno.ECONNRESET:
print('WARN: connection was reset during post_image')
print('ERROR: post_image failed ' + inboxUrl + ' ' +
str(headers) + ' ' + str(ex))
return None
except ValueError as ex:
print('WARN: error during post_image ' + str(ex))
return None
if postResult:
return postResult.text
return None
def download_image(session, base_dir: str, url: str,
image_filename: str, debug: bool,
force: bool = False) -> bool:
"""Downloads an image with an expected mime type
"""
if not url:
return None
# try different image types
imageFormats = {
'png': 'png',
'jpg': 'jpeg',
'jpeg': 'jpeg',
'gif': 'gif',
'svg': 'svg+xml',
'webp': 'webp',
'avif': 'avif',
'ico': 'x-icon'
}
sessionHeaders = None
for imFormat, mimeType in imageFormats.items():
if url.endswith('.' + imFormat) or \
'.' + imFormat + '?' in url:
sessionHeaders = {
'Accept': 'image/' + mimeType
}
break
if not sessionHeaders:
if debug:
print('download_image: no session headers')
return False
if not os.path.isfile(image_filename) or force:
try:
if debug:
print('Downloading image url: ' + url)
result = session.get(url,
headers=sessionHeaders,
params=None)
if result.status_code < 200 or \
result.status_code > 202:
if debug:
print('Image download failed with status ' +
str(result.status_code))
# remove partial download
if os.path.isfile(image_filename):
try:
os.remove(image_filename)
except OSError:
print('EX: download_image unable to delete ' +
image_filename)
else:
with open(image_filename, 'wb') as f:
f.write(result.content)
if debug:
print('Image downloaded from ' + url)
return True
except Exception as ex:
print('EX: Failed to download image: ' +
str(url) + ' ' + str(ex))
return False
def download_image_any_mime_type(session, url: str,
timeoutSec: int, debug: bool):
"""http GET for an image with any mime type
"""
mimeType = None
content_type = None
result = None
sessionHeaders = {
'Accept': 'image/x-icon, image/png, image/webp, image/jpeg, image/gif'
}
try:
result = session.get(url, headers=sessionHeaders, timeout=timeoutSec)
except requests.exceptions.RequestException as ex:
print('ERROR: download_image_any_mime_type failed: ' +
str(url) + ', ' + str(ex))
return None, None
except ValueError as ex:
print('ERROR: download_image_any_mime_type failed: ' +
str(url) + ', ' + str(ex))
return None, None
except SocketError as ex:
if ex.errno == errno.ECONNRESET:
print('WARN: download_image_any_mime_type failed, ' +
'connection was reset ' + str(ex))
return None, None
if not result:
return None, None
if result.status_code != 200:
print('WARN: download_image_any_mime_type: ' + url +
' failed with error code ' + str(result.status_code))
return None, None
if result.headers.get('content-type'):
content_type = result.headers['content-type']
elif result.headers.get('Content-type'):
content_type = result.headers['Content-type']
elif result.headers.get('Content-Type'):
content_type = result.headers['Content-Type']
if not content_type:
return None, None
imageFormats = {
'ico': 'x-icon',
'png': 'png',
'jpg': 'jpeg',
'jpeg': 'jpeg',
'gif': 'gif',
'svg': 'svg+xml',
'webp': 'webp',
'avif': 'avif'
}
for imFormat, mType in imageFormats.items():
if 'image/' + mType in content_type:
mimeType = 'image/' + mType
return result.content, mimeType