aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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: