Compare commits
No commits in common. "9140cedad05c27a496ace56d72c8bfc0b79f098b" and "0b1a98fb73035c1600cccdfb714bebc05873ecc8" have entirely different histories.
9140cedad0
...
0b1a98fb73
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,4 +1,3 @@
|
||||||
*.pyc
|
*.pyc
|
||||||
*.egg-info/
|
|
||||||
build/
|
/venv/
|
||||||
dist/
|
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
TODO
|
TODO
|
||||||
----------------------------------------
|
----------------------------------------
|
||||||
|
more UT
|
||||||
setup.py
|
setup.py
|
||||||
make client cert gen configurable
|
make client cert gen configurable
|
||||||
|
|
||||||
|
|
|
@ -43,5 +43,4 @@ def main():
|
||||||
save_cert_stash(cert_stash, cert_stash_path)
|
save_cert_stash(cert_stash, cert_stash_path)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
main()
|
||||||
main()
|
|
||||||
|
|
|
@ -301,7 +301,7 @@ 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 identities is None:
|
if not identities:
|
||||||
browser.set_status_error("Can't load identities.")
|
browser.set_status_error("Can't load identities.")
|
||||||
return None
|
return None
|
||||||
browser.identities = identities
|
browser.identities = identities
|
||||||
|
@ -352,9 +352,8 @@ def create_identity(browser: Browser, url: str):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
browser.set_status("Generating certificate…")
|
browser.set_status("Generating certificate…")
|
||||||
gen_command = browser.config["generate_client_cert_command"]
|
|
||||||
try:
|
try:
|
||||||
mangled_name = create_certificate(url, common_name, gen_command)
|
mangled_name = create_certificate(url, common_name)
|
||||||
except ClientCertificateException as exc:
|
except ClientCertificateException as exc:
|
||||||
browser.set_status_error(exc.message)
|
browser.set_status_error(exc.message)
|
||||||
return None
|
return None
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
import os.path
|
||||||
|
|
||||||
|
|
||||||
DEFAULT_CONFIG = {
|
DEFAULT_CONFIG = {
|
||||||
|
@ -16,25 +16,13 @@ DEFAULT_CONFIG = {
|
||||||
"external_command_default": ["xdg-open"],
|
"external_command_default": ["xdg-open"],
|
||||||
"home": "bebop:welcome",
|
"home": "bebop:welcome",
|
||||||
"render_mode": "fancy",
|
"render_mode": "fancy",
|
||||||
"generate_client_cert_command": [
|
|
||||||
"openssl", "req",
|
|
||||||
"-newkey", "rsa:4096",
|
|
||||||
"-nodes",
|
|
||||||
"-keyform", "PEM",
|
|
||||||
"-keyout", "{key_path}",
|
|
||||||
"-x509",
|
|
||||||
"-days", "28140", # https://www.youtube.com/watch?v=F9L4q-0Pi4E
|
|
||||||
"-outform", "PEM",
|
|
||||||
"-out", "{cert_path}",
|
|
||||||
"-subj", "/CN={common_name}",
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
RENDER_MODES = ("fancy", "dumb")
|
RENDER_MODES = ("fancy", "dumb")
|
||||||
|
|
||||||
|
|
||||||
def load_config(config_path: Path):
|
def load_config(config_path):
|
||||||
if not config_path.is_file():
|
if not os.path.isfile(config_path):
|
||||||
create_default_config(config_path)
|
create_default_config(config_path)
|
||||||
return DEFAULT_CONFIG
|
return DEFAULT_CONFIG
|
||||||
|
|
||||||
|
@ -42,11 +30,9 @@ def load_config(config_path: Path):
|
||||||
with open(config_path, "rt") as config_file:
|
with open(config_path, "rt") as config_file:
|
||||||
config = json.load(config_file)
|
config = json.load(config_file)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
abs_path = config_path.absolute()
|
logging.error(f"Could not read config file {config_path}: {exc}")
|
||||||
logging.error(f"Could not read config file {abs_path}: {exc}")
|
|
||||||
except ValueError as exc:
|
except ValueError as exc:
|
||||||
abs_path = config_path.absolute()
|
logging.error(f"Could not parse config file {config_path}: {exc}")
|
||||||
logging.error(f"Could not parse config file {abs_path}: {exc}")
|
|
||||||
else:
|
else:
|
||||||
# Fill missing values with defaults.
|
# Fill missing values with defaults.
|
||||||
for key, value in DEFAULT_CONFIG.items():
|
for key, value in DEFAULT_CONFIG.items():
|
||||||
|
@ -56,14 +42,7 @@ def load_config(config_path: Path):
|
||||||
return DEFAULT_CONFIG
|
return DEFAULT_CONFIG
|
||||||
|
|
||||||
|
|
||||||
def create_default_config(config_path: Path):
|
def create_default_config(config_path):
|
||||||
config_dir = config_path.parent
|
|
||||||
if not config_dir.is_dir():
|
|
||||||
try:
|
|
||||||
config_dir.mkdir(parents=True)
|
|
||||||
except OSError as exc:
|
|
||||||
logging.error(f"Could not create config dir {config_dir}: {exc}")
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
with open(config_path, "wt") as config_file:
|
with open(config_path, "wt") as config_file:
|
||||||
json.dump(DEFAULT_CONFIG, config_file, indent=2)
|
json.dump(DEFAULT_CONFIG, config_file, indent=2)
|
||||||
|
|
|
@ -68,7 +68,6 @@ Here are the available options:
|
||||||
* external_command_default (see note 1): default command to open files.
|
* external_command_default (see note 1): default command to open files.
|
||||||
* home (string): home page.
|
* home (string): home page.
|
||||||
* render_mode (string): default render mode to use ("fancy" or "dumb").
|
* render_mode (string): default render mode to use ("fancy" or "dumb").
|
||||||
* generate_client_cert_command (see note 3): command to generate a client cert.
|
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
|
|
||||||
|
@ -76,8 +75,6 @@ Notes:
|
||||||
|
|
||||||
2: the external_commands dict maps MIME types to commands just as above. For example, if you want to open video files with VLC and audio files in Clementine, you can use the following dict: `{"audio": ["clementine"], "video": ["vlc"]}`. For now only "main" MIME types are supported, i.e. you cannot specify precise types like "audio/flac", just "audio".
|
2: the external_commands dict maps MIME types to commands just as above. For example, if you want to open video files with VLC and audio files in Clementine, you can use the following dict: `{"audio": ["clementine"], "video": ["vlc"]}`. For now only "main" MIME types are supported, i.e. you cannot specify precise types like "audio/flac", just "audio".
|
||||||
|
|
||||||
3: the generate_client_cert_command uses the same format as other commands (specified in note 1 above), with the exception that if the strings "{cert_path}", "{key_path}" or "{common_name}" are present in any string for the list, they will be replaced respectively by the certificate output path, the key output path and the CN to use.
|
|
||||||
|
|
||||||
Your current configuration is:
|
Your current configuration is:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
|
@ -83,23 +83,24 @@ def get_cert_and_key(cert_id: str):
|
||||||
return directory / f"{cert_id}.crt", directory / f"{cert_id}.key"
|
return directory / f"{cert_id}.crt", directory / f"{cert_id}.key"
|
||||||
|
|
||||||
|
|
||||||
def create_certificate(url: str, common_name: str, gen_command: list):
|
def create_certificate(url: str, common_name: str):
|
||||||
"""Create a secure self-signed certificate using system's OpenSSL."""
|
"""Create a secure self-signed certificate using system's OpenSSL."""
|
||||||
identities_path = get_identities_path()
|
identities_path = get_identities_path()
|
||||||
mangled_name = get_mangled_name(url, common_name)
|
mangled_name = get_mangled_name(url, common_name)
|
||||||
cert_path = identities_path / f"{mangled_name}.crt"
|
cert_path = identities_path / f"{mangled_name}.crt"
|
||||||
key_path = identities_path / f"{mangled_name}.key"
|
key_path = identities_path / f"{mangled_name}.key"
|
||||||
|
command = [
|
||||||
command = []
|
"openssl", "req",
|
||||||
for part in gen_command:
|
"-newkey", "rsa:4096",
|
||||||
if "{key_path}" in part:
|
"-nodes",
|
||||||
part = part.format(key_path=str(key_path))
|
"-keyform", "PEM",
|
||||||
if "{cert_path}" in part:
|
"-keyout", str(key_path),
|
||||||
part = part.format(cert_path=str(cert_path))
|
"-x509",
|
||||||
if "{common_name}" in part:
|
"-days", "28140", # https://www.youtube.com/watch?v=F9L4q-0Pi4E
|
||||||
part = part.format(common_name=common_name)
|
"-outform", "PEM",
|
||||||
command.append(part)
|
"-out", str(cert_path),
|
||||||
|
"-subj", f"/CN={common_name}",
|
||||||
|
]
|
||||||
try:
|
try:
|
||||||
subprocess.check_call(
|
subprocess.check_call(
|
||||||
command,
|
command,
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
"""Gemini protocol implementation."""
|
"""Gemini protocol implementation."""
|
||||||
|
|
||||||
import logging
|
|
||||||
import re
|
import re
|
||||||
import socket
|
import socket
|
||||||
import ssl
|
import ssl
|
||||||
|
@ -158,15 +157,7 @@ class Request:
|
||||||
# Setup TLS.
|
# Setup TLS.
|
||||||
context = Request.get_ssl_context()
|
context = Request.get_ssl_context()
|
||||||
if self.identity:
|
if self.identity:
|
||||||
try:
|
|
||||||
context.load_cert_chain(*self.identity)
|
context.load_cert_chain(*self.identity)
|
||||||
except FileNotFoundError as exc:
|
|
||||||
sock.close()
|
|
||||||
self.state = Request.STATE_CONNECTION_FAILED
|
|
||||||
self.error = "Could not load identity files."
|
|
||||||
logging.error(f"Failed to load identity files {self.identity}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
self.ssock = context.wrap_socket(sock, server_hostname=hostname)
|
self.ssock = context.wrap_socket(sock, server_hostname=hostname)
|
||||||
except OSError as exc:
|
except OSError as exc:
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
import unittest
|
|
||||||
|
|
||||||
from ..mime import MimeType, DEFAULT_CHARSET, DEFAULT_MIME_TYPE
|
|
||||||
|
|
||||||
|
|
||||||
class TestMime(unittest.TestCase):
|
|
||||||
|
|
||||||
def test_from_str(self):
|
|
||||||
self.assertIsNone(MimeType.from_str(""))
|
|
||||||
self.assertIsNone(MimeType.from_str("dumb"))
|
|
||||||
self.assertIsNone(MimeType.from_str("dumb;dumber"))
|
|
||||||
self.assertIsNone(MimeType.from_str("123456"))
|
|
||||||
|
|
||||||
mime = MimeType.from_str("a/b")
|
|
||||||
self.assertEqual(mime.main_type, "a")
|
|
||||||
self.assertEqual(mime.sub_type, "b")
|
|
||||||
self.assertEqual(mime.parameters, {})
|
|
||||||
|
|
||||||
mime = MimeType.from_str("text/gemini")
|
|
||||||
self.assertEqual(mime.main_type, "text")
|
|
||||||
self.assertEqual(mime.sub_type, "gemini")
|
|
||||||
self.assertEqual(mime.parameters, {})
|
|
||||||
|
|
||||||
mime = MimeType.from_str("text/gemini;lang=en")
|
|
||||||
self.assertEqual(mime.main_type, "text")
|
|
||||||
self.assertEqual(mime.sub_type, "gemini")
|
|
||||||
self.assertEqual(mime.parameters, {"lang": "en"})
|
|
||||||
mime = MimeType.from_str("text/gemini ;lang=en")
|
|
||||||
self.assertEqual(mime.parameters, {"lang": "en"})
|
|
22
setup.cfg
22
setup.cfg
|
@ -1,22 +0,0 @@
|
||||||
[metadata]
|
|
||||||
name = bebop-browser
|
|
||||||
version = 0.0.1
|
|
||||||
description = Terminal browser for Gemini
|
|
||||||
long_description = file: README.md
|
|
||||||
license = GPLv3
|
|
||||||
author = dece
|
|
||||||
author-email = shgck@pistache.land
|
|
||||||
home-page = https://git.dece.space/Dece/Bebop
|
|
||||||
classifiers =
|
|
||||||
Environment :: Console
|
|
||||||
Programming Language :: Python :: 3
|
|
||||||
Programming Language :: Python :: 3.7
|
|
||||||
|
|
||||||
[options]
|
|
||||||
packages = bebop, bebop.browser
|
|
||||||
python_requires = >= 3.7
|
|
||||||
setup_requires = setuptools >= 38.3.0
|
|
||||||
|
|
||||||
[options.entry_points]
|
|
||||||
console_scripts =
|
|
||||||
bebop = bebop.__main__:main
|
|
Reference in a new issue