about summary refs log tree commit diff
diff options
context:
space:
mode:
authorNguyễn Gia Phong <cnx@loang.net>2025-05-22 14:04:54 +0900
committerNguyễn Gia Phong <cnx@loang.net>2025-05-22 14:04:54 +0900
commitbc50a2b206d611c56bdf9bb29a825ea1abba6bd9 (patch)
treed71ee6efc26440470b79fcfeda4a8d12b1bfe4e8
parentc3ec223b35e71a6d44e5ec01c55d4aa9746bc3a5 (diff)
downloadscadere-bc50a2b206d611c56bdf9bb29a825ea1abba6bd9.tar.gz
Show more details about the certificates
-rw-r--r--src/scadere/__main__.py2
-rw-r--r--src/scadere/listen.py60
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: