diff --git a/documentation/Initialization Process.drawio b/documentation/Initialization Process.drawio new file mode 100644 index 00000000..63727ce8 --- /dev/null +++ b/documentation/Initialization Process.drawio @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/documentation/example.ini b/documentation/example.ini deleted file mode 100644 index 33792108..00000000 --- a/documentation/example.ini +++ /dev/null @@ -1,45 +0,0 @@ -[DEFAULT] -test=val - -[Network] -interface=eth0 -subnet=10.0.0.0/24 -netmask=255.255.255.0 -ip=10.0.0.1 -dhcp-ranges=10.0.0.100-10.0.0.254 -dhcp-lease=12h -no-dhcp=false -nat-requests=true -nat-interface=eth1 -ftp_directory=/ftpboot -tftp_directory=/tftpboot -ftp_port=20 -tftp_port=69 -gateways=10.0.0.50 -dns-servers=1.1.1.1 -no-dns=false # - -[OS] -os=debian -version=12 - -[UEFI] -uefi=false - -[Users] -root-login=true -root-password= - -username=alice -password= -ssh-key= -sudoer=true - -[Applications] - -ssh= -sql= -mongodb= - -[DNSMasq Overrides] -authoritative=false diff --git a/documentation/example.yml b/documentation/example.yml index 30cba056..40689f11 100644 --- a/documentation/example.yml +++ b/documentation/example.yml @@ -19,6 +19,33 @@ Network: ftp: directory: "/ftp" ftp-port: 20 + server: + # the prefered port, server will throw an exception if port is occupied on + # the specified ip above. If no IP was specifed, attempts to bind to all interfaces + port: 15206 + # the user and group the server should drop privileges to if ran as root + # if absent will run the server as the running user + # if not ran as root or the specified user, a warning will be emitted but the server will still run + # if group is absent, will attempt to use a group with the same name as user + user: genisys + group: genisys + # the working directory for the server process. Used to canonicalize any paths from the ansible section. + # The specified user needs, at minimum, execute (--x) access to the directory. If any of the ansible files + # are stored there the user will need read (r--) permissions on those files or read-write (rw-) access for + # the inventory file. They will also need write (-w-) access to the working-directory if the inventory file + # does not exist. Defaults to the user's home directory + working-directory: /srv/genisys + # run the server using SSL + # either both or neither of cert and key need specified if enabled + # if neither are present, genisys generates a self-signed key on first run + # the self-signed cert will be created at $GENISYS_CERT_STORE or /etc/genisys/ssl/ + # if the ssl section is missing, the server will be ran without ssl + ssl: + cert: /path/to/fullchain.pem + key: /path/to/privkey.pem + # nessecary if the privkey is encrypted, the first line of the file will be used as the passphrase + # when decrytping the key, after stripping any newline + password-file: /path/to/key/password OS: os: "debian" version-name: bookworm @@ -35,9 +62,23 @@ Users: - "test2.pub" sudoer: true Applications: + - curl DNSMasq Overrides: authoritative: false Scripts: script-dir: "/scripts" move-all: true script-list: ["script1.sh"] +# configure settings for the ansible integration +ansible: + # this specifies which inventory file is updated by the server process + # is created if it does not exist. If it does exist, must be writable by the server user. + # If the inventory file already exists, it should be defined in YAML format. A 'genisys' + # section will be added to the file if it does not already exist. + inventory: /var/genisys/inventory + # the ssh private key used to run the playbooks. The corresponding public key should be specified + # in the Users section above. Must be readable by the server user + ssh-key: /etc/genisys/ssh/id_rsa + # list of paths to playbooks to run when the server receives a HELLO + playbooks: + - /etc/genisys/playbooks/firstrun.yaml diff --git a/genisys/main.py b/genisys/main.py index acc32e85..a3400a53 100644 --- a/genisys/main.py +++ b/genisys/main.py @@ -14,6 +14,7 @@ from genisys.modules.firstboot.hello import Hello from genisys.modules.firstboot.service import Service import genisys.config_parser as cp +import genisys.server MODULES = [ OSDownload, @@ -57,12 +58,6 @@ def generate_config(file, root="."): mod = module(file) mod.install(root) -def daemon(): - """Monitor the config file for changes""" - print("Starting daemon...") - - raise NotImplementedError - def run(subcommand, args): """Parse command line options and run the relevant helper method""" # Config Parser @@ -74,6 +69,8 @@ def run(subcommand, args): install_config(yaml_parser, args.root) elif subcommand == "generate": generate_config(yaml_parser, args.root) + elif subcommand == "server": + genisys.server.run(yaml_parser) def main(): @@ -92,12 +89,12 @@ def main(): generate_parser = subparsers.add_parser( "generate", help="Generate the configuration files." ) - daemon_parser = subparsers.add_parser( - "daemon", help="Monitor the config file for changes." + server_parser = subparsers.add_parser( + "server", help="Run the server to listen for new clients." ) # Flags for all subparsers - for subparser in [validate_parser, install_parser, generate_parser]: + for subparser in [validate_parser, install_parser, generate_parser, server_parser]: subparser.add_argument( "-f", "--file", diff --git a/genisys/server/__init__.py b/genisys/server/__init__.py new file mode 100644 index 00000000..a7b1eed8 --- /dev/null +++ b/genisys/server/__init__.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python3 +import ssl +import pwd +import grp +import os +import sys +from warnings import warn +from signal import signal, SIGTERM +from typing_extensions import Dict, TypedDict, cast +from genisys.config_parser import YAMLParser +from genisys.server.inventory import Inventory +from genisys.server.http import GenisysHTTPServer, GenisysHTTPRequestHandler +import genisys.server.tls + +DEFAULT_PORT = 15206 +DEFAULT_INVENTORY = "/etc/ansible/hosts" + +ServerOptions = TypedDict("ServerOptions", { + "port": int, + "user": str, + "group": str, + "working-directory": str, + "ssl": Dict[str, str] +}) + +def run(config: YAMLParser): + """Drops priviledges, creates the server (with SSL, if applicable), then waits for requests""" + # parse config + network = config.get_section("Network") + server_options = cast(ServerOptions, network.get("server", {}) or {}) + + # drop priviledges + try: + server_user = drop_priviledges(server_options) + except PermissionError: + warn("Unable to drop privledges to the specified user. Continuing as current user.") + server_user = pwd.getpwuid(os.getuid()) + + # change working directory + workdir = server_options.get("working-directory", server_user.pw_dir) + os.chdir(workdir) + + # install additional data for the server to use + ansible_cfg = config.get_section("ansible") + inventory_path = ansible_cfg.get("inventory", DEFAULT_INVENTORY) + + # create a server + server_address = network.get('ip', '') + server_port = server_options.get("port", DEFAULT_PORT) + httpd = GenisysHTTPServer((server_address, server_port), Inventory(inventory_path), config) + + # apply TLS if applicable + if 'ssl' in server_options: + ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2 + ssl_cert = genisys.server.tls.get_keychain(server_options['ssl'] or {}) + ssl_context.load_cert_chain(**ssl_cert) + httpd.socket = ssl_context.wrap_socket(httpd.socket) + + + # run until SIGTERM is caught + def sigterm_handler(*_): + print("killed") + del httpd.inventory + sys.exit(SIGTERM) + signal(SIGTERM, sigterm_handler) + httpd.serve_forever() + +def drop_priviledges(config: ServerOptions) -> pwd.struct_passwd: + """Attempts to drop the priviledges to that of the specified users, returns false on failure""" + if 'user' not in config: + warn("No user specified. Continuing as current user.") + return pwd.getpwuid(os.geteuid()) + + grpnam = config.get('group', config['user']) + uid = pwd.getpwnam(config['user']) + gid = grp.getgrnam(grpnam) + + os.setuid(uid.pw_uid) + os.setgid(gid.gr_gid) + return uid diff --git a/genisys/server/http.py b/genisys/server/http.py new file mode 100644 index 00000000..d3d5cbe6 --- /dev/null +++ b/genisys/server/http.py @@ -0,0 +1,68 @@ +import sys +import json +import subprocess +from http.server import HTTPServer, BaseHTTPRequestHandler +from typing_extensions import Self, Tuple, cast +from genisys.config_parser import YAMLParser +from genisys.server.inventory import Inventory + +def generate_response(status_code: int, message: str) -> bytes: + """Generates a JSON formated HTTP response with the given status and message""" + statuses = {200: "OK", 400: "Bad Request", 500: "Internal Server Error"} + response_body = '{"message":"' + message + '"}' + response = f"HTTP/1.1 {status_code} {statuses[status_code]}\n" + response += f"Content-Length: {len(response_body)}\n" + response += "Content-Type: application/json\n" + response += "\n" + response += response_body + return bytes(response, encoding='utf-8') + +class GenisysHTTPServer(HTTPServer): + """Subclass to manage shared state between server requests""" + def __init__(self: Self, bind: Tuple[str, int], inventory: Inventory, config: YAMLParser): + super().__init__(bind, GenisysHTTPRequestHandler) + self.inventory = inventory + self.config = config + +class GenisysHTTPRequestHandler(BaseHTTPRequestHandler): + """Process client "hello"s by running ansible playbooks on a received POST""" + def do_POST(self: Self): + """On POST, store the client in the Ansible inventory and run playbooks""" + try: + # cast server since we know it will only be called from Genisys + server = cast(GenisysHTTPServer, self.server) + + # get the request body + content_length = int(self.headers['Content-Length']) + body = json.loads(self.rfile.read(content_length)) + + # validate the declared IP and hostname + if body['ip'] != self.client_address[0] \ + or server.inventory.get(body['hostname']) is not None: + self.wfile.write(generate_response(400, 'Declared IP is not valid.')) + + # add the host to the inventory file + server.inventory.add_host(body['hostname'], { 'ansible_host': body['ip'] }) + + # run the playbooks + ansible_cmd = [ + 'ansible-playbook', + '--inventory', + server.inventory.filename, + '--limit', + body['hostname'] + ] + for playbook in server.config.get_section('ansible').get('playbooks', []): + ansible_cmd.append(playbook) + subprocess.run(ansible_cmd, check=True) + + # send success back to the client + self.wfile.write(generate_response(200, 'success')) + except Exception as e: + print(f"ERROR: {e}", file=sys.stderr) + self.wfile.write( + generate_response( + 500, + 'Internal server error. Check logs for details.' + ) + ) diff --git a/genisys/server/inventory.py b/genisys/server/inventory.py new file mode 100644 index 00000000..61a07d7a --- /dev/null +++ b/genisys/server/inventory.py @@ -0,0 +1,39 @@ +from typing_extensions import Self, Optional, Dict +import yaml + +class Inventory: + """Manages a Yaml-formatted Ansible inventory file by keeping an in-memory "shadow" hosts file + up to date with the on-disk representation. + """ + instances = {} + + def __init__(self: Self, filepath: str): + """Open a file handle and initialize the in-memory representation.""" + # create a new instance + self.filename = filepath + self.fd = open(filepath, 'r+', encoding='utf-8') + self.contents = yaml.safe_load(self.fd) + + # ensure the inventory group exists + if 'genisys' not in self.contents: + self.contents['genisys'] = {'hosts': {}} + + def __del__(self: Self): + """Close the file handle if this is the last remaining instance.""" + self.fd.close() + + def get(self: Self, hostname: str) -> Optional[Dict]: + """Returns None if the host doesn't exist or the settings if it does""" + return self.contents['genisys']['hosts'].get(hostname) + + def add_host(self: Self, hostname: str, settings: Optional[Dict] = None): + """Adds the host to the inventory, updating the in-memory and on-disk file.""" + # Update the in-memory representation + self.contents['genisys']['hosts'][hostname] = settings or {} + + # Truncate the current inventory file + self.fd.seek(0) + + # Write the new invetory to disk + yaml.dump(self.contents, self.fd) + self.fd.flush() diff --git a/genisys/server/tls.py b/genisys/server/tls.py new file mode 100644 index 00000000..2859b218 --- /dev/null +++ b/genisys/server/tls.py @@ -0,0 +1,104 @@ +import os +from pathlib import Path +from datetime import datetime, timedelta +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives import hashes +from cryptography import x509 +from cryptography.x509.oid import NameOID +from typing_extensions import TypedDict, Union, NotRequired, Dict + +CERTIFICATE_STORE_PATH = Path(os.getenv('GENISYS_CERT_STORE', '/etc/genisys/ssl')) + +class CertChainArgs(TypedDict): + """Typed Dict for arguments to be applied to the SSLContext#load_cert_chain method""" + keyfile: str + certfile: str + password: NotRequired[Union[str, None]] + +def get_keychain(config: Dict[str, str]) -> CertChainArgs: + """Converts from the configured filenames to the nessecary format to pass to + SSLContext.load_cert_chain, creating the keypair if nessecary + """ + if ('cert' not in config and 'key' in config) \ + or ('cert' in config and 'key' not in config): + raise ValueError("Only one of SSL Certificate or Key have been specified") + + if 'cert' in config: + passwd = None + if 'password-file' in config: + with open(config['password-file'], 'r', encoding='utf-8') as fd: + passwd = fd.readline().strip('\n') + return { + 'certfile': config['cert'], + 'keyfile': config['key'], + 'password': passwd + } + + # generate or use the cert if applicable + cert_path = CERTIFICATE_STORE_PATH / 'cert.pem' + key_path = CERTIFICATE_STORE_PATH / 'key.pem' + + if not (cert_path.exists() and key_path.exists()): + # generate new keys + CERTIFICATE_STORE_PATH.mkdir(parents=True, exist_ok=True) + generate_keypair(cert_path, key_path) + + return { 'certfile': str(cert_path), 'keyfile': str(key_path) } + +def generate_keypair(cert_path: Path, key_path: Path): + """Generates a new x509 keypair into the specified cert and key files""" + # thanks ChatGPT + private_key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + ) + + # Create a self-signed certificate + subject = issuer = x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, 'genisys.internal'), + ]) + + cert = x509.CertificateBuilder().subject_name( + subject + ).issuer_name( + issuer + ).public_key( + private_key.public_key() + ).serial_number( + x509.random_serial_number() + ).not_valid_before( + datetime.utcnow() + ).not_valid_after( + datetime.utcnow() + timedelta(days=365) + ).sign( + private_key, hashes.SHA256() + ) + + # Serialize the private key and certificate to PEM format + pem_private_key = private_key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=serialization.NoEncryption() + ) + + pem_cert = cert.public_bytes(encoding=serialization.Encoding.PEM) + + # ensure files exist and have correct permissions + cert_perms = 0o644 + if cert_path.exists(): + cert_path.chmod(cert_perms) + else: + cert_path.touch(cert_perms) + + key_perms = 0o600 + if key_path.exists(): + key_path.chmod(key_perms) + else: + key_path.touch(key_perms) + + # write the certs to their files + with cert_path.open('w') as cert_file: + cert_file.write(pem_cert.decode()) + with key_path.open('w') as key_file: + key_file.write(pem_private_key.decode()) diff --git a/poetry.lock b/poetry.lock index d4494dc7..aa16b34f 100644 --- a/poetry.lock +++ b/poetry.lock @@ -25,6 +25,70 @@ files = [ {file = "certifi-2023.11.17.tar.gz", hash = "sha256:9b469f3a900bf28dc19b8cfbf8019bf47f7fdd1a65a1d4ffb98fc14166beb4d1"}, ] +[[package]] +name = "cffi" +version = "1.16.0" +description = "Foreign Function Interface for Python calling C code." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, + {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, + {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, + {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, + {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, + {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, + {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, + {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, + {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, + {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, + {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, + {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, + {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, + {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, + {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, + {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, + {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, + {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, + {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, + {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, + {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, + {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, + {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, + {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, + {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, + {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, +] + +[package.dependencies] +pycparser = "*" + [[package]] name = "charset-normalizer" version = "3.3.2" @@ -135,6 +199,60 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "cryptography" +version = "42.0.2" +description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." +optional = false +python-versions = ">=3.7" +files = [ + {file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:701171f825dcab90969596ce2af253143b93b08f1a716d4b2a9d2db5084ef7be"}, + {file = "cryptography-42.0.2-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:61321672b3ac7aade25c40449ccedbc6db72c7f5f0fdf34def5e2f8b51ca530d"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea2c3ffb662fec8bbbfce5602e2c159ff097a4631d96235fcf0fb00e59e3ece4"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b15c678f27d66d247132cbf13df2f75255627bcc9b6a570f7d2fd08e8c081d2"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:8e88bb9eafbf6a4014d55fb222e7360eef53e613215085e65a13290577394529"}, + {file = "cryptography-42.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a047682d324ba56e61b7ea7c7299d51e61fd3bca7dad2ccc39b72bd0118d60a1"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:36d4b7c4be6411f58f60d9ce555a73df8406d484ba12a63549c88bd64f7967f1"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:a00aee5d1b6c20620161984f8ab2ab69134466c51f58c052c11b076715e72929"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:b97fe7d7991c25e6a31e5d5e795986b18fbbb3107b873d5f3ae6dc9a103278e9"}, + {file = "cryptography-42.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5fa82a26f92871eca593b53359c12ad7949772462f887c35edaf36f87953c0e2"}, + {file = "cryptography-42.0.2-cp37-abi3-win32.whl", hash = "sha256:4b063d3413f853e056161eb0c7724822a9740ad3caa24b8424d776cebf98e7ee"}, + {file = "cryptography-42.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:841ec8af7a8491ac76ec5a9522226e287187a3107e12b7d686ad354bb78facee"}, + {file = "cryptography-42.0.2-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:55d1580e2d7e17f45d19d3b12098e352f3a37fe86d380bf45846ef257054b242"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28cb2c41f131a5758d6ba6a0504150d644054fd9f3203a1e8e8d7ac3aea7f73a"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9097a208875fc7bbeb1286d0125d90bdfed961f61f214d3f5be62cd4ed8a446"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:44c95c0e96b3cb628e8452ec060413a49002a247b2b9938989e23a2c8291fc90"}, + {file = "cryptography-42.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:2f9f14185962e6a04ab32d1abe34eae8a9001569ee4edb64d2304bf0d65c53f3"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:09a77e5b2e8ca732a19a90c5bca2d124621a1edb5438c5daa2d2738bfeb02589"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ad28cff53f60d99a928dfcf1e861e0b2ceb2bc1f08a074fdd601b314e1cc9e0a"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:130c0f77022b2b9c99d8cebcdd834d81705f61c68e91ddd614ce74c657f8b3ea"}, + {file = "cryptography-42.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa3dec4ba8fb6e662770b74f62f1a0c7d4e37e25b58b2bf2c1be4c95372b4a33"}, + {file = "cryptography-42.0.2-cp39-abi3-win32.whl", hash = "sha256:3dbd37e14ce795b4af61b89b037d4bc157f2cb23e676fa16932185a04dfbf635"}, + {file = "cryptography-42.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:8a06641fb07d4e8f6c7dda4fc3f8871d327803ab6542e33831c7ccfdcb4d0ad6"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:087887e55e0b9c8724cf05361357875adb5c20dec27e5816b653492980d20380"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a7ef8dd0bf2e1d0a27042b231a3baac6883cdd5557036f5e8df7139255feaac6"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4383b47f45b14459cab66048d384614019965ba6c1a1a141f11b5a551cace1b2"}, + {file = "cryptography-42.0.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:fbeb725c9dc799a574518109336acccaf1303c30d45c075c665c0793c2f79a7f"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:320948ab49883557a256eab46149df79435a22d2fefd6a66fe6946f1b9d9d008"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:5ef9bc3d046ce83c4bbf4c25e1e0547b9c441c01d30922d812e887dc5f125c12"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:52ed9ebf8ac602385126c9a2fe951db36f2cb0c2538d22971487f89d0de4065a"}, + {file = "cryptography-42.0.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:141e2aa5ba100d3788c0ad7919b288f89d1fe015878b9659b307c9ef867d3a65"}, + {file = "cryptography-42.0.2.tar.gz", hash = "sha256:e0ec52ba3c7f1b7d813cd52649a5b3ef1fc0d433219dc8c93827c57eab6cf888"}, +] + +[package.dependencies] +cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} + +[package.extras] +docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] +docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] +nox = ["nox"] +pep8test = ["check-sdist", "click", "mypy", "ruff"] +sdist = ["build"] +ssh = ["bcrypt (>=3.1.5)"] +test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] +test-randomorder = ["pytest-randomly"] + [[package]] name = "dill" version = "0.3.7" @@ -306,6 +424,17 @@ files = [ docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.1)", "sphinx-autodoc-typehints (>=1.24)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4)", "pytest-cov (>=4.1)", "pytest-mock (>=3.11.1)"] +[[package]] +name = "pycparser" +version = "2.21" +description = "C parser in Python" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"}, + {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"}, +] + [[package]] name = "pylint" version = "3.0.2" @@ -361,6 +490,7 @@ files = [ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, @@ -479,4 +609,4 @@ zstd = ["zstandard (>=0.18.0)"] [metadata] lock-version = "2.0" python-versions = "^3.8" -content-hash = "e452be00ca557e3ce23715a2c04400e24074aaf41e58abd1f785a0df0a12353f" +content-hash = "b8ed7d08cf146fff304d0e123079b0bb4a4726510552b4279d5c2aba22197500" diff --git a/pyproject.toml b/pyproject.toml index a8185d65..ed8185e0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ typing-extensions = "^4.8.0" passlib = "^1.7.4" textwrap3 = "^0.9.2" requests = "^2.31.0" +cryptography = "^42.0.2" [tool.poetry.group.dev.dependencies] pylint = "^3.0.1"