diff options
author | Nguyễn Gia Phong <cnx@loang.net> | 2025-05-28 20:34:25 +0900 |
---|---|---|
committer | Nguyễn Gia Phong <cnx@loang.net> | 2025-05-28 20:34:25 +0900 |
commit | 42b2fb1d329f614822d3c5b080185199e4be98e2 (patch) | |
tree | 007fd02a8c29477b90883ff65c760e764c9e705a | |
parent | 37e9fc1b09112d59ec9d4d38f0ab449979f7e5c0 (diff) | |
download | scadere-42b2fb1d329f614822d3c5b080185199e4be98e2.tar.gz |
Unuse subcommand for help2man
-rw-r--r-- | pyproject.toml | 10 | ||||
-rw-r--r-- | src/scadere/__init__.py | 64 | ||||
-rw-r--r-- | src/scadere/__main__.py | 111 | ||||
-rw-r--r-- | src/scadere/check.py | 42 | ||||
-rw-r--r-- | src/scadere/listen.py | 39 | ||||
-rw-r--r-- | tst/test_check.py | 8 | ||||
-rw-r--r-- | tst/test_listen.py | 8 |
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, |