aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--pyproject.toml10
-rw-r--r--src/scadere/__init__.py64
-rw-r--r--src/scadere/__main__.py111
-rw-r--r--src/scadere/check.py42
-rw-r--r--src/scadere/listen.py39
-rw-r--r--tst/test_check.py8
-rw-r--r--tst/test_listen.py8
7 files changed, 150 insertions, 132 deletions
diff --git a/pyproject.toml b/pyproject.toml
index d11b2ba..3e2d251 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -21,8 +21,10 @@ classifiers = [
'Programming Language :: Python',
'Topic :: Utilities' ]
dynamic = [ 'version' ]
-urls = { Source = 'https://trong.loang.net/scadere' }
-scripts = { scadere = 'scadere.__main__:main' }
+
+[project.urls]
+Source = 'https://trong.loang.net/scadere'
+Inbox = 'https://loa.loang.net/chung'
[project.optional-dependencies]
dev = [ 'coverage',
@@ -32,6 +34,10 @@ dev = [ 'coverage',
'pytest-asyncio',
'trustme >= 1.2.0' ]
+[project.scripts]
+scadere-check = 'scadere.check:main'
+scadere-listen = 'scadere.listen:main'
+
[tool.pytest.ini_options]
asyncio_mode = 'auto'
asyncio_default_fixture_loop_scope = 'function'
diff --git a/src/scadere/__init__.py b/src/scadere/__init__.py
index 2a73e5d..7becab1 100644
--- a/src/scadere/__init__.py
+++ b/src/scadere/__init__.py
@@ -1,2 +1,62 @@
-__all__ = ['__version__']
-__version__ = '0.0.1'
+# Root package, with some helpers
+# Copyright (C) 2025 Nguyễn Gia Phong
+#
+# This file is part of scadere.
+#
+# Scadere is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as published
+# by the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# Scadere is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with scadere. If not, see <https://www.gnu.org/licenses/>.
+
+from argparse import HelpFormatter
+
+__all__ = ['__version__', 'GNUHelpFormatter', 'NetLoc']
+__version__ = '0.1.0'
+
+
+class GNUHelpFormatter(HelpFormatter):
+ """Help formatter for ArgumentParser following GNU Coding Standards."""
+
+ def add_usage(self, usage, actions, groups, prefix='Usage: '):
+ """Substitute 'Usage:' for 'usage:'."""
+ super().add_usage(usage, actions, groups, prefix)
+
+ def start_section(self, heading):
+ """Substitute 'Options:' for 'options:'."""
+ super().start_section(heading.capitalize())
+
+ def _format_action_invocation(self, action):
+ """Format --long-option=argument."""
+ if not action.option_strings or action.nargs is not None:
+ return super()._format_action_invocation(action)
+ arg = self._format_args(action,
+ self._get_default_metavar_for_optional(action))
+ return ', '.join(f"{opt}{'=' if opt.startswith('--') else ' '}{arg}"
+ for opt in action.option_strings)
+
+ def add_argument(self, action):
+ """Suppress positional arguments."""
+ if action.option_strings:
+ super().add_argument(action)
+
+
+class NetLoc:
+ """Network location with default port."""
+
+ def __init__(self, default_port):
+ self.default_port = default_port
+
+ def __call__(self, string):
+ """Return hostname and port from given netloc."""
+ if ':' not in string:
+ return string, self.default_port
+ hostname, port = string.rsplit(':', 1)
+ return hostname, int(port) # ValueError to be handled by argparse
diff --git a/src/scadere/__main__.py b/src/scadere/__main__.py
deleted file mode 100644
index 3467f95..0000000
--- a/src/scadere/__main__.py
+++ /dev/null
@@ -1,111 +0,0 @@
-# Executable entry point
-# Copyright (C) 2025 Nguyễn Gia Phong
-#
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU Affero General Public License as published
-# by the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU Affero General Public License for more details.
-#
-# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see <https://www.gnu.org/licenses/>.
-
-from argparse import ArgumentParser, FileType, HelpFormatter
-from asyncio import run
-from datetime import datetime, timedelta, timezone
-from pathlib import Path
-from sys import stdout
-
-from . import __version__
-from .check import check
-from .listen import listen
-
-
-class GNUHelpFormatter(HelpFormatter):
- """Help formatter for ArgumentParser following GNU Coding Standards."""
-
- def add_usage(self, usage, actions, groups, prefix='Usage: '):
- """Substitute 'Usage:' for 'usage:'."""
- super().add_usage(usage, actions, groups, prefix)
-
- def start_section(self, heading):
- """Substitute 'Options:' for 'options:'."""
- super().start_section(heading.capitalize())
-
- def _format_action_invocation(self, action):
- """Format --long-option=argument."""
- if not action.option_strings or action.nargs is not None:
- return super()._format_action_invocation(action)
- arg = self._format_args(action,
- self._get_default_metavar_for_optional(action))
- return ', '.join(f"{opt}{'=' if opt.startswith('--') else ' '}{arg}"
- for opt in action.option_strings)
-
- def add_argument(self, action):
- """Suppress positional arguments."""
- if action.option_strings:
- super().add_argument(action)
-
-
-class NetLoc:
- def __init__(self, default_port):
- self.default_port = default_port
-
- def __call__(self, string):
- """Return hostname and port from given netloc."""
- if ':' not in string:
- return string, self.default_port
- hostname, port = string.rsplit(':', 1)
- return hostname, int(port) # ValueError to be handled by argparse
-
-
-def main():
- """Parse arguments and launch subprogram."""
- parser = ArgumentParser(prog='scadere', allow_abbrev=False,
- formatter_class=GNUHelpFormatter)
- parser.add_argument('-v', '--version', action='version',
- version=f'%(prog)s {__version__}')
- subparsers = parser.add_subparsers(help='subcommand help', required=True)
-
- check_description = ('Check TLS certificate expiration of HOST,'
- ' where PORT defaults to 443.')
- check_parser = subparsers.add_parser('check',
- description=check_description,
- formatter_class=GNUHelpFormatter)
- check_parser.set_defaults(subcommand='check')
- check_parser.add_argument('netloc', metavar='HOST[:PORT]',
- nargs='+', type=NetLoc(443))
- check_parser.add_argument('-d', '--days', type=float, default=7,
- help='days before expiration (default to 7)')
- check_parser.add_argument('-o', '--output', metavar='PATH',
- type=FileType('w'), default=stdout,
- help='output file (default to stdout)')
-
- listen_description = ('Serve the TLS certificate expiration feed'
- ' from INPUT file for base URL at HOST:PORT,'
- ' where HOST defaults to localhost and PORT'
- ' is selected randomly if not specified.')
- listen_parser = subparsers.add_parser('listen',
- description=listen_description,
- 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='?',
- type=NetLoc(None), default=('localhost', None))
- listen_parser.set_defaults(subcommand='listen')
-
- args = parser.parse_args()
- if args.subcommand == 'check':
- with args.output:
- after = datetime.now(tz=timezone.utc) + timedelta(days=args.days)
- check(args.netloc, after, args.output)
- elif args.subcommand == 'listen':
- run(listen(args.certs, args.base_url, *args.netloc))
-
-
-if __name__ == '__main__':
- main()
diff --git a/src/scadere/check.py b/src/scadere/check.py
index fec0b22..7ef2d4a 100644
--- a/src/scadere/check.py
+++ b/src/scadere/check.py
@@ -1,27 +1,33 @@
# TLS certificate expiration checker
# Copyright (C) 2025 Nguyễn Gia Phong
#
-# This program is free software: you can redistribute it and/or modify
+# This file is part of scadere.
+#
+# Scadere is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
-# This program is distributed in the hope that it will be useful,
+# Scadere is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see <https://www.gnu.org/licenses/>.
+# along with scadere. If not, see <https://www.gnu.org/licenses/>.
+from argparse import ArgumentParser, FileType
from base64 import urlsafe_b64encode as base64
+from datetime import datetime, timedelta, timezone
from email.utils import parsedate_to_datetime as parsedate
from itertools import chain
from socket import AF_INET, socket
from ssl import create_default_context as tls_context
-from sys import stderr
+from sys import stderr, stdout
+
+from . import __version__, GNUHelpFormatter, NetLoc
-__all__ = ['check']
+__all__ = ['check', 'main']
def check(netlocs, after, output, fake_ca=None):
@@ -58,3 +64,29 @@ def check(netlocs, after, output, fake_ca=None):
hostname, port, cert['serialNumber'],
base64(ca.encode()).decode(),
file=output)
+
+
+def main():
+ """Run TLS checker."""
+ description = ('Check TLS certificate expiration of HOST,'
+ ' where PORT defaults to 443.')
+ parser = ArgumentParser(prog='scadere-check', allow_abbrev=False,
+ description=description,
+ formatter_class=GNUHelpFormatter)
+ parser.add_argument('-v', '--version', action='version',
+ version=f'%(prog)s {__version__}')
+ parser.add_argument('netloc', metavar='HOST[:PORT]',
+ nargs='+', type=NetLoc(443))
+ parser.add_argument('-d', '--days', type=float, default=7,
+ help='days before expiration (default to 7)')
+ parser.add_argument('-o', '--output', metavar='PATH',
+ type=FileType('w'), default=stdout,
+ help='output file (default to stdout)')
+ args = parser.parse_args()
+ with args.output:
+ after = datetime.now(tz=timezone.utc) + timedelta(days=args.days)
+ check(args.netloc, after, args.output)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/src/scadere/listen.py b/src/scadere/listen.py
index 1cf822a..4870e2f 100644
--- a/src/scadere/listen.py
+++ b/src/scadere/listen.py
@@ -1,31 +1,35 @@
# HTTP server for Atom feed of TLS certificate expirations
# Copyright (C) 2025 Nguyễn Gia Phong
#
-# This program is free software: you can redistribute it and/or modify
+# This file is part of scadere.
+#
+# Scadere is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
-# This program is distributed in the hope that it will be useful,
+# Scadere is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see <https://www.gnu.org/licenses/>.
+# along with scadere. If not, see <https://www.gnu.org/licenses/>.
-from asyncio import start_server
+from argparse import ArgumentParser
+from asyncio import run, start_server
from base64 import urlsafe_b64decode as from_base64
from datetime import datetime
from functools import partial
+from pathlib import Path
from urllib.parse import parse_qs, urljoin, urlsplit
from xml.etree.ElementTree import (Element as xml_element,
SubElement as xml_subelement,
indent, tostring as str_from_xml)
-from . import __version__
+from . import __version__, GNUHelpFormatter, NetLoc
-__all__ = ['listen']
+__all__ = ['listen', 'main', 'parse_summary']
def parse_summary(line):
@@ -162,3 +166,26 @@ async def listen(certs, base_url, host, port): # pragma: no cover
print('Serving on', end=' ')
print(*(socket.getsockname() for socket in server.sockets), sep=', ')
await server.serve_forever()
+
+
+def main():
+ """Launch server."""
+ description = ('Serve the TLS certificate expiration feed'
+ ' from INPUT file for base URL at HOST:PORT,'
+ ' where HOST defaults to localhost and PORT'
+ ' is selected randomly if not specified.')
+ parser = ArgumentParser(prog='scadere-listen', allow_abbrev=False,
+ description=description,
+ formatter_class=GNUHelpFormatter)
+ parser.add_argument('-v', '--version', action='version',
+ version=f'%(prog)s {__version__}')
+ parser.add_argument('certs', metavar='INPUT', type=Path)
+ parser.add_argument('base_url', metavar='URL')
+ parser.add_argument('netloc', metavar='[HOST][:PORT]', nargs='?',
+ type=NetLoc(None), default=('localhost', None))
+ args = parser.parse_args()
+ run(listen(args.certs, args.base_url, *args.netloc))
+
+
+if __name__ == '__main__':
+ main()
diff --git a/tst/test_check.py b/tst/test_check.py
index bfbe927..b9a89ff 100644
--- a/tst/test_check.py
+++ b/tst/test_check.py
@@ -1,18 +1,20 @@
# Tests for the TLS client
# Copyright (C) 2025 Nguyễn Gia Phong
#
-# This program is free n redistribute it and/or modify
+# This file is part of scadere.
+#
+# Scadere is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
-# This program is distributed in the hope that it will be useful,
+# Scadere is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see <https://www.gnu.org/licenses/>.
+# along with scadere. If not, see <https://www.gnu.org/licenses/>.
from asyncio import get_running_loop, start_server
from base64 import urlsafe_b64encode as base64
diff --git a/tst/test_listen.py b/tst/test_listen.py
index 1409834..ec98140 100644
--- a/tst/test_listen.py
+++ b/tst/test_listen.py
@@ -1,18 +1,20 @@
# Tests for the HTTP server
# Copyright (C) 2025 Nguyễn Gia Phong
#
-# This program is free software: you can redistribute it and/or modify
+# This file is part of scadere.
+#
+# Scadere is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
-# This program is distributed in the hope that it will be useful,
+# Scadere is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
-# along with this program. If not, see <https://www.gnu.org/licenses/>.
+# along with scadere. If not, see <https://www.gnu.org/licenses/>.
from asyncio import open_connection, start_server
from base64 import (urlsafe_b64decode as from_base64,