diff options
author | Nguyễn Gia Phong <cnx@loang.net> | 2025-05-22 14:04:54 +0900 |
---|---|---|
committer | Nguyễn Gia Phong <cnx@loang.net> | 2025-05-22 14:04:54 +0900 |
commit | bc50a2b206d611c56bdf9bb29a825ea1abba6bd9 (patch) | |
tree | d71ee6efc26440470b79fcfeda4a8d12b1bfe4e8 | |
parent | c3ec223b35e71a6d44e5ec01c55d4aa9746bc3a5 (diff) | |
download | scadere-bc50a2b206d611c56bdf9bb29a825ea1abba6bd9.tar.gz |
Show more details about the certificates
-rw-r--r-- | src/scadere/__main__.py | 2 | ||||
-rw-r--r-- | src/scadere/listen.py | 60 |
2 files changed, 45 insertions, 17 deletions
diff --git a/src/scadere/__main__.py b/src/scadere/__main__.py index b33d4d7..3467f95 100644 --- a/src/scadere/__main__.py +++ b/src/scadere/__main__.py @@ -94,7 +94,7 @@ def main(): formatter_class=GNUHelpFormatter) listen_parser.add_argument('certs', metavar='INPUT', type=Path) listen_parser.add_argument('base_url', metavar='URL') - listen_parser.add_argument('netloc', metavar='HOST[:PORT]', nargs='?', + listen_parser.add_argument('netloc', metavar='[HOST][:PORT]', nargs='?', type=NetLoc(None), default=('localhost', None)) listen_parser.set_defaults(subcommand='listen') diff --git a/src/scadere/listen.py b/src/scadere/listen.py index 5576986..cb31b46 100644 --- a/src/scadere/listen.py +++ b/src/scadere/listen.py @@ -15,9 +15,10 @@ # along with this program. If not, see <https://www.gnu.org/licenses/>. from asyncio import start_server +from base64 import urlsafe_b64encode as base64 from datetime import datetime from functools import partial -from urllib.parse import parse_qs, quote, urljoin, urlsplit +from urllib.parse import parse_qs, urljoin, urlsplit from xml.etree.ElementTree import (Element as xml_element, SubElement as xml_subelement, indent, tostring as xml_to_string) @@ -25,19 +26,35 @@ from xml.etree.ElementTree import (Element as xml_element, __all__ = ['listen'] +def path(hostname, port, issuer, serial): + """Return the relative URL for the given certificate's details.""" + issuer_b64 = base64(issuer.encode()).decode() + return f'{hostname}/{port}/{issuer_b64}/{serial}' + + +def body(not_before, not_after, hostname, port, serial, issuer): + """Describe the given certificate in XHTML.""" + return (('p', 'TLS certificate information'), + ('dl', + ('dt', 'Domain'), ('dd', hostname), + ('dt', 'Port'), ('dd', port), + ('dt', 'Issuer'), ('dd', issuer), + ('dt', 'Serial number'), ('dd', serial), + ('dt', 'Valid from'), ('dd', not_before), + ('dt', 'Valid until'), ('dd', not_after))) + + def entry(base_url, cert): """Construct Atom entry for the given TLS certificate.""" not_before, not_after, hostname, port, serial, issuer = cert - url = urljoin(base_url, quote(f'{hostname}/{port}/{issuer}/{serial}')) + url = urljoin(base_url, path(hostname, port, issuer, serial)) return ('entry', ('author', ('name', issuer)), - ('content', {'type': 'text'}, - (f'TLS certificate for {hostname}:{port}' - f' issued by {issuer} will expire at {not_after}')), + ('content', {'type': 'xhtml'}, + ('div', {'xmlns': 'http://www.w3.org/1999/xhtml'}, *body(*cert))), ('id', url), ('link', {'rel': 'alternate', 'type': 'text/plain', 'href': url}), - ('title', (f'TLS certificate for {hostname}:{port}' - f' will expire at {not_after}')), + ('title', (f'TLS cert for {hostname} will expire at {not_after}')), ('updated', not_before)) @@ -64,14 +81,13 @@ async def handle(certs, base_url, reader, writer): """Handle HTTP request.""" summaries = tuple(cert.rstrip().split(maxsplit=5) for cert in certs.read_text().splitlines()) - lookup = {quote(f'/{hostname}/{port}/{issuer}/{serial}'): - (not_before, not_after) + lookup = {f'/{path(hostname, port, issuer, serial)}': + (not_before, not_after, hostname, port, serial, issuer) for not_before, not_after, hostname, port, serial, issuer in summaries} request = await reader.readuntil(b'\r\n') url = request.removeprefix(b'GET ').rsplit(b' HTTP/', 1)[0] url_parts = urlsplit(url.decode()) - path = url_parts.path domains = tuple(parse_qs(url_parts.query).get('domain', [''])) if not request.startswith(b'GET '): @@ -80,7 +96,7 @@ async def handle(certs, base_url, reader, writer): writer.close() await writer.wait_closed() return - elif path == '/': # Atom feed + elif url_parts.path == '/': # Atom feed writer.write(b'HTTP/1.1 200 OK\r\n') writer.write(b'Content-Type: application/atom+xml\r\n') feed = xml(('feed', {'xmlns': 'http://www.w3.org/2005/Atom'}, @@ -94,12 +110,24 @@ async def handle(certs, base_url, reader, writer): default_namespace=None).encode() writer.write(f'Content-Length: {len(content)}\r\n\r\n'.encode()) writer.write(content) - elif path in lookup: # accessible Atom entry's link/ID + elif url_parts.path in lookup: # accessible Atom entry's link/ID writer.write(b'HTTP/1.1 200 OK\r\n') - writer.write(b'Content-Type: text/plain\r\n') - not_before, not_after = lookup[path] - content = (f'TLS certificate is valid' - f' from {not_before} until {not_after}\n').encode() + writer.write(b'Content-Type: application/xhtml+xml\r\n') + (not_before, not_after, + hostname, port, serial, issuer) = lookup[url_parts.path] + page = xml(('html', {'xmlns': 'http://www.w3.org/1999/xhtml', + 'lang': 'en'}, + ('head', + ('meta', {'name': 'color-scheme', + 'content': 'dark light'}), + ('meta', {'name': 'viewport', + 'content': ('width=device-width,' + 'initial-scale=1.0')}), + ('title', f'TLS certificate - {hostname}:{port}')), + ('body', *body(not_before, not_after, + hostname, port, serial, issuer)))) + content = xml_to_string(page, 'unicode', xml_declaration=True, + default_namespace=None).encode() writer.write(f'Content-Length: {len(content)}\r\n\r\n'.encode()) writer.write(content) else: |