identity: present cert instead of waiting for 60

This commit is contained in:
dece 2021-05-13 01:24:29 +02:00
parent 57f01720d6
commit 8b1561e689
3 changed files with 40 additions and 24 deletions

View file

@ -17,7 +17,9 @@ from bebop.colors import ColorPair, init_colors
from bebop.command_line import CommandLine from bebop.command_line import CommandLine
from bebop.external import open_external_program from bebop.external import open_external_program
from bebop.help import HELP_PAGE from bebop.help import HELP_PAGE
from bebop.fs import get_identities_list_path
from bebop.history import History from bebop.history import History
from bebop.identity import load_identities
from bebop.links import Links from bebop.links import Links
from bebop.mime import MimeType from bebop.mime import MimeType
from bebop.mouse import ButtonState from bebop.mouse import ButtonState
@ -43,12 +45,13 @@ class Browser:
- status_data: 3-uple of status text, color pair and attributes of the - status_data: 3-uple of status text, color pair and attributes of the
status line, used to reset status after an error. status line, used to reset status after an error.
- history: an History object. - history: an History object.
- cache: a dict containing cached pages - cache: a dict containing cached pages.
- special_pages: a dict containing page names used with "bebop" scheme; - special_pages: a dict containing page names used with "bebop" scheme;
values are dicts as well: the "open" key maps to a callable to use when values are dicts as well: the "open" key maps to a callable to use when
the page is accessed, and the optional "source" key maps to callable the page is accessed, and the optional "source" key maps to callable
returning the page source path. returning the page source path.
- last_download: tuple of MimeType and path, or None. - last_download: tuple of MimeType and path, or None.
- identities: identities map.
""" """
def __init__(self, config, cert_stash): def __init__(self, config, cert_stash):
@ -65,6 +68,7 @@ class Browser:
self.cache = {} self.cache = {}
self.special_pages = self.setup_special_pages() self.special_pages = self.setup_special_pages()
self.last_download: Optional[Tuple[MimeType, Path]] = None self.last_download: Optional[Tuple[MimeType, Path]] = None
self.identities = load_identities(get_identities_list_path()) or {}
self._current_url = "" self._current_url = ""
@property @property

View file

@ -26,7 +26,7 @@ def open_gemini_url(
url: str, url: str,
redirects: int =0, redirects: int =0,
use_cache: bool =True, use_cache: bool =True,
identity=None cert_and_key=None
) -> Optional[str]: ) -> Optional[str]:
"""Open a Gemini URL and set the formatted response as content. """Open a Gemini URL and set the formatted response as content.
@ -50,7 +50,7 @@ def open_gemini_url(
- url: a valid URL with Gemini scheme to open. - url: a valid URL with Gemini scheme to open.
- redirects: current amount of redirections done to open the initial URL. - redirects: current amount of redirections done to open the initial URL.
- use_cache: if true, look up if the page is cached before requesting it. - use_cache: if true, look up if the page is cached before requesting it.
- identity: if not None, a tuple of paths to a client cert/key to use. - cert_and_key: if not None, a tuple of paths to a client cert/key to use.
Returns: Returns:
The final successfully handled URL on success, None otherwise. Redirected The final successfully handled URL on success, None otherwise. Redirected
@ -64,13 +64,20 @@ def open_gemini_url(
loading_message = f"{loading_message_verb} {url}" loading_message = f"{loading_message_verb} {url}"
browser.set_status(loading_message) browser.set_status(loading_message)
# If this URL used to request an identity, provide it.
if not cert_and_key:
url_identities = get_identities_for_url(browser.identities, url)
identity = select_identity(url_identities)
if identity:
cert_and_key = get_cert_and_key(identity["id"])
if use_cache and url in browser.cache: if use_cache and url in browser.cache:
browser.load_page(browser.cache[url]) browser.load_page(browser.cache[url])
browser.current_url = url browser.current_url = url
browser.set_status(url) browser.set_status(url)
return url return url
req = Request(url, browser.stash, identity=identity) req = Request(url, browser.stash, identity=cert_and_key)
connect_timeout = browser.config["connect_timeout"] connect_timeout = browser.config["connect_timeout"]
connected = req.connect(connect_timeout) connected = req.connect(connect_timeout)
if not connected: if not connected:
@ -281,21 +288,20 @@ def _handle_cert_required(
The result of `open_gemini_url` with the client certificate provided. The result of `open_gemini_url` with the client certificate provided.
""" """
identities = load_identities(get_identities_list_path()) identities = load_identities(get_identities_list_path())
if isinstance(identities, str): if not identities:
browser.set_status_error(f"Can't load identities: {identities}") browser.set_status_error(f"Can't load identities.")
return None return None
browser.identities = identities
url_identities = get_identities_for_url(identities, url) url_identities = get_identities_for_url(browser.identities, url)
if not url_identities: if not url_identities:
identity = create_identity(browser, url) identity = create_identity(browser, url)
if not identity: if not identity:
return None return None
identities[url] = [identity] browser.identities[url] = [identity]
save_identities(identities, get_identities_list_path()) save_identities(browser.identities, get_identities_list_path())
else: else:
# TODO support multiple identities; for now we just use the first identity = select_identity(url_identities)
# available.
identity = url_identities[0]
cert_path, key_path = get_cert_and_key(identity["id"]) cert_path, key_path = get_cert_and_key(identity["id"])
return open_gemini_url( return open_gemini_url(
@ -303,10 +309,16 @@ def _handle_cert_required(
url, url,
redirects=redirects + 1, redirects=redirects + 1,
use_cache=False, use_cache=False,
identity=(cert_path, key_path) cert_and_key=(cert_path, key_path)
) )
def select_identity(identities: list):
"""Let user select the appropriate identity among candidates."""
# TODO support multiple identities; for now we just use the first available.
return identities[0] if identities else None
def create_identity(browser: Browser, url: str): def create_identity(browser: Browser, url: str):
"""Walk the user through identity creation. """Walk the user through identity creation.

View file

@ -33,24 +33,24 @@ from typing import Optional, Union
from bebop.fs import get_identities_path, get_user_data_path from bebop.fs import get_identities_path, get_user_data_path
def load_identities(identities_path: Path) -> Union[dict, str]: def load_identities(identities_path: Path) -> Optional[dict]:
"""Return saved identities, else an error str.""" """Return saved identities or None on error."""
identities = {} identities = {}
try: try:
with open(identities_path, "rt") as identities_file: with open(identities_path, "rt") as identities_file:
identities = json.load(identities_file) identities = json.load(identities_file)
except (OSError, ValueError) as exc: except (OSError, ValueError) as exc:
return f"Failed to load identities '{identities_path}': {exc}" return None
return identities return identities
def save_identities(identities: dict, identities_path: Path): def save_identities(identities: dict, identities_path: Path):
"""Save the certificate stash. Return True on success, else an error str.""" """Save the certificate stash. Return True on success."""
try: try:
with open(identities_path, "wt") as identities_file: with open(identities_path, "wt") as identities_file:
json.dump(identities, identities_file) json.dump(identities, identities_file)
except (OSError, ValueError) as exc: except (OSError, ValueError) as exc:
return f"Failed to save identities '{identities_path}': {exc}" return False
return True return True
@ -102,8 +102,8 @@ def create_certificate(url: str, common_name: str):
try: try:
subprocess.check_call( subprocess.check_call(
command, command,
# stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
# stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
) )
except subprocess.CalledProcessError as exc: except subprocess.CalledProcessError as exc:
error = "Could not create certificate: " + str(exc) error = "Could not create certificate: " + str(exc)