protocol: catch connection/SSL errors
This commit is contained in:
parent
15037ec0a6
commit
5ef9a0430a
|
@ -34,6 +34,8 @@ class Request:
|
||||||
STATE_UNTRUSTED_CERT = 5
|
STATE_UNTRUSTED_CERT = 5
|
||||||
# Valid and trusted cert: proceed.
|
# Valid and trusted cert: proceed.
|
||||||
STATE_OK = 6
|
STATE_OK = 6
|
||||||
|
# Connection failed.
|
||||||
|
STATE_CONNECTION_FAILED = 7
|
||||||
|
|
||||||
def __init__(self, url, cert_stash):
|
def __init__(self, url, cert_stash):
|
||||||
self.url = url
|
self.url = url
|
||||||
|
@ -43,6 +45,7 @@ class Request:
|
||||||
self.ssock = None
|
self.ssock = None
|
||||||
self.cert = None
|
self.cert = None
|
||||||
self.cert_status = None
|
self.cert_status = None
|
||||||
|
self.error = ""
|
||||||
|
|
||||||
def connect(self):
|
def connect(self):
|
||||||
"""Connect to a Gemini server and return a RequestEventType.
|
"""Connect to a Gemini server and return a RequestEventType.
|
||||||
|
@ -73,9 +76,21 @@ class Request:
|
||||||
return False
|
return False
|
||||||
self.payload += LINE_TERM
|
self.payload += LINE_TERM
|
||||||
|
|
||||||
|
try:
|
||||||
|
sock = socket.create_connection((hostname, port))
|
||||||
|
except socket.gaierror as exc:
|
||||||
|
self.state = Request.STATE_CONNECTION_FAILED
|
||||||
|
self.error = exc.strerror
|
||||||
|
return False
|
||||||
|
|
||||||
context = Request.get_ssl_context()
|
context = Request.get_ssl_context()
|
||||||
sock = socket.create_connection((hostname, port))
|
try:
|
||||||
self.ssock = context.wrap_socket(sock)
|
self.ssock = context.wrap_socket(sock)
|
||||||
|
except OSError as exc:
|
||||||
|
self.state = Request.STATE_CONNECTION_FAILED
|
||||||
|
self.error = exc.strerror
|
||||||
|
return False
|
||||||
|
|
||||||
der = self.ssock.getpeercert(binary_form=True)
|
der = self.ssock.getpeercert(binary_form=True)
|
||||||
self.cert_status, self.cert = \
|
self.cert_status, self.cert = \
|
||||||
validate_cert(der, hostname, self.cert_stash)
|
validate_cert(der, hostname, self.cert_stash)
|
||||||
|
|
|
@ -135,7 +135,7 @@ class Screen:
|
||||||
|
|
||||||
def set_status_error(self, text):
|
def set_status_error(self, text):
|
||||||
"""Set an error message in the status bar."""
|
"""Set an error message in the status bar."""
|
||||||
self.status_data = f"Error: {text}", ColorPair.ERROR
|
self.status_data = text, ColorPair.ERROR
|
||||||
self.refresh_status_line()
|
self.refresh_status_line()
|
||||||
|
|
||||||
def open_url(self, url, redirections=0):
|
def open_url(self, url, redirections=0):
|
||||||
|
@ -145,7 +145,7 @@ class Screen:
|
||||||
current URL, unless there is no current URL yet.
|
current URL, unless there is no current URL yet.
|
||||||
"""
|
"""
|
||||||
if redirections > 5:
|
if redirections > 5:
|
||||||
self.set_status_error(f"too many redirections ({url})")
|
self.set_status_error(f"Too many redirections ({url}).")
|
||||||
return
|
return
|
||||||
if self.current_url:
|
if self.current_url:
|
||||||
parts = parse_url(url)
|
parts = parse_url(url)
|
||||||
|
@ -157,7 +157,7 @@ class Screen:
|
||||||
url = join_url(self.current_url, url)
|
url = join_url(self.current_url, url)
|
||||||
self.open_gemini_url(sanitize_url(url), redirections)
|
self.open_gemini_url(sanitize_url(url), redirections)
|
||||||
else:
|
else:
|
||||||
self.set_status_error(f"protocol {parts.scheme} not supported")
|
self.set_status_error(f"Protocol {parts.scheme} not supported.")
|
||||||
|
|
||||||
def open_gemini_url(self, url, redirections=0, history=True):
|
def open_gemini_url(self, url, redirections=0, history=True):
|
||||||
"""Open a Gemini URL and set the formatted response as content."""
|
"""Open a Gemini URL and set the formatted response as content."""
|
||||||
|
@ -166,13 +166,16 @@ class Screen:
|
||||||
connected = req.connect()
|
connected = req.connect()
|
||||||
if not connected:
|
if not connected:
|
||||||
if req.state == Request.STATE_ERROR_CERT:
|
if req.state == Request.STATE_ERROR_CERT:
|
||||||
error = f"certificate was missing or corrupt ({url})"
|
error = f"Certificate was missing or corrupt ({url})."
|
||||||
self.set_status_error(error)
|
self.set_status_error(error)
|
||||||
elif req.state == Request.STATE_UNTRUSTED_CERT:
|
elif req.state == Request.STATE_UNTRUSTED_CERT:
|
||||||
self.set_status_error(f"certificate has been changed ({url})")
|
self.set_status_error(f"Certificate has been changed ({url}).")
|
||||||
# TODO propose the user ways to handle this.
|
# TODO propose the user ways to handle this.
|
||||||
|
elif req.state == Request.STATE_CONNECTION_FAILED:
|
||||||
|
error = f": {req.error}" if req.error else ""
|
||||||
|
self.set_status_error(f"Connection failed ({url}){error}.")
|
||||||
else:
|
else:
|
||||||
self.set_status_error(f"connection failed ({url})")
|
self.set_status_error(f"Connection failed ({url}).")
|
||||||
return
|
return
|
||||||
|
|
||||||
if req.state == Request.STATE_INVALID_CERT:
|
if req.state == Request.STATE_INVALID_CERT:
|
||||||
|
@ -186,7 +189,7 @@ class Screen:
|
||||||
|
|
||||||
response = Response.parse(req.proceed())
|
response = Response.parse(req.proceed())
|
||||||
if not response:
|
if not response:
|
||||||
self.set_status_error(f"server response parsing failed ({url})")
|
self.set_status_error(f"Server response parsing failed ({url}).")
|
||||||
return
|
return
|
||||||
|
|
||||||
if response.code == 20:
|
if response.code == 20:
|
||||||
|
@ -198,7 +201,8 @@ class Screen:
|
||||||
elif response.generic_code == 30 and response.meta:
|
elif response.generic_code == 30 and response.meta:
|
||||||
self.open_url(response.meta, redirections=redirections + 1)
|
self.open_url(response.meta, redirections=redirections + 1)
|
||||||
elif response.generic_code in (40, 50):
|
elif response.generic_code in (40, 50):
|
||||||
self.set_status_error(response.meta or Response.code.name)
|
error = f"Server error: {response.meta or Response.code.name}."
|
||||||
|
self.set_status_error(error)
|
||||||
|
|
||||||
def load_page(self, gemtext: bytes):
|
def load_page(self, gemtext: bytes):
|
||||||
"""Load Gemtext data as the current page."""
|
"""Load Gemtext data as the current page."""
|
||||||
|
@ -280,7 +284,7 @@ class Screen:
|
||||||
try:
|
try:
|
||||||
self.open_link(links, int(link_input))
|
self.open_link(links, int(link_input))
|
||||||
except ValueError:
|
except ValueError:
|
||||||
self.set_status_error("invalid link ID")
|
self.set_status_error("Invalid link ID.")
|
||||||
|
|
||||||
def _validate_link_digit(self, ch: int, links, max_digits: int):
|
def _validate_link_digit(self, ch: int, links, max_digits: int):
|
||||||
"""Handle input chars to be used as link ID."""
|
"""Handle input chars to be used as link ID."""
|
||||||
|
@ -313,7 +317,7 @@ class Screen:
|
||||||
def open_link(self, links, link_id: int):
|
def open_link(self, links, link_id: int):
|
||||||
"""Open the link with this link ID."""
|
"""Open the link with this link ID."""
|
||||||
if not link_id in links:
|
if not link_id in links:
|
||||||
self.set_status_error(f"unknown link ID {link_id}.")
|
self.set_status_error(f"Unknown link ID {link_id}.")
|
||||||
return
|
return
|
||||||
self.open_url(links[link_id])
|
self.open_url(links[link_id])
|
||||||
|
|
||||||
|
|
Reference in a new issue