mirror of
https://github.com/aykhans/AzSuicideDataVisualization.git
synced 2025-07-02 06:22:25 +00:00
first commit
This commit is contained in:
14
.venv/Lib/site-packages/zmq/auth/__init__.py
Normal file
14
.venv/Lib/site-packages/zmq/auth/__init__.py
Normal file
@ -0,0 +1,14 @@
|
||||
"""Utilities for ZAP authentication.
|
||||
|
||||
To run authentication in a background thread, see :mod:`zmq.auth.thread`.
|
||||
For integration with the tornado eventloop, see :mod:`zmq.auth.ioloop`.
|
||||
For integration with the asyncio event loop, see :mod:`zmq.auth.asyncio`.
|
||||
|
||||
Authentication examples are provided in the pyzmq codebase, under
|
||||
`/examples/security/`.
|
||||
|
||||
.. versionadded:: 14.1
|
||||
"""
|
||||
|
||||
from .base import *
|
||||
from .certs import *
|
59
.venv/Lib/site-packages/zmq/auth/asyncio.py
Normal file
59
.venv/Lib/site-packages/zmq/auth/asyncio.py
Normal file
@ -0,0 +1,59 @@
|
||||
"""ZAP Authenticator integrated with the asyncio IO loop.
|
||||
|
||||
.. versionadded:: 15.2
|
||||
"""
|
||||
|
||||
# Copyright (C) PyZMQ Developers
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import asyncio
|
||||
import warnings
|
||||
from typing import Any, Optional
|
||||
|
||||
import zmq
|
||||
from zmq.asyncio import Poller
|
||||
|
||||
from .base import Authenticator
|
||||
|
||||
|
||||
class AsyncioAuthenticator(Authenticator):
|
||||
"""ZAP authentication for use in the asyncio IO loop"""
|
||||
|
||||
__poller: Optional[Poller]
|
||||
__task: Any
|
||||
zap_socket: "zmq.asyncio.Socket"
|
||||
|
||||
def __init__(self, context: Optional["zmq.Context"] = None, loop: Any = None):
|
||||
super().__init__(context)
|
||||
if loop is not None:
|
||||
warnings.warn(f"{self.__class__.__name__}(loop) is deprecated and ignored")
|
||||
self.__poller = None
|
||||
self.__task = None
|
||||
|
||||
async def __handle_zap(self) -> None:
|
||||
while True:
|
||||
if self.__poller is None:
|
||||
break
|
||||
events = await self.__poller.poll()
|
||||
if self.zap_socket in dict(events):
|
||||
msg = await self.zap_socket.recv_multipart()
|
||||
self.handle_zap_message(msg)
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start ZAP authentication"""
|
||||
super().start()
|
||||
self.__poller = Poller()
|
||||
self.__poller.register(self.zap_socket, zmq.POLLIN)
|
||||
self.__task = asyncio.ensure_future(self.__handle_zap())
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop ZAP authentication"""
|
||||
if self.__task:
|
||||
self.__task.cancel()
|
||||
if self.__poller:
|
||||
self.__poller.unregister(self.zap_socket)
|
||||
self.__poller = None
|
||||
super().stop()
|
||||
|
||||
|
||||
__all__ = ["AsyncioAuthenticator"]
|
443
.venv/Lib/site-packages/zmq/auth/base.py
Normal file
443
.venv/Lib/site-packages/zmq/auth/base.py
Normal file
@ -0,0 +1,443 @@
|
||||
"""Base implementation of 0MQ authentication."""
|
||||
|
||||
# Copyright (C) PyZMQ Developers
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import logging
|
||||
import os
|
||||
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
||||
|
||||
import zmq
|
||||
from zmq.error import _check_version
|
||||
from zmq.utils import z85
|
||||
|
||||
from .certs import load_certificates
|
||||
|
||||
CURVE_ALLOW_ANY = '*'
|
||||
VERSION = b'1.0'
|
||||
|
||||
|
||||
class Authenticator:
|
||||
"""Implementation of ZAP authentication for zmq connections.
|
||||
|
||||
This authenticator class does not register with an event loop. As a result,
|
||||
you will need to manually call `handle_zap_message`::
|
||||
|
||||
auth = zmq.Authenticator()
|
||||
auth.allow("127.0.0.1")
|
||||
auth.start()
|
||||
while True:
|
||||
auth.handle_zap_msg(auth.zap_socket.recv_multipart()
|
||||
|
||||
Alternatively, you can register `auth.zap_socket` with a poller.
|
||||
|
||||
Since many users will want to run ZAP in a way that does not block the
|
||||
main thread, other authentication classes (such as :mod:`zmq.auth.thread`)
|
||||
are provided.
|
||||
|
||||
Note:
|
||||
|
||||
- libzmq provides four levels of security: default NULL (which the Authenticator does
|
||||
not see), and authenticated NULL, PLAIN, CURVE, and GSSAPI, which the Authenticator can see.
|
||||
- until you add policies, all incoming NULL connections are allowed.
|
||||
(classic ZeroMQ behavior), and all PLAIN and CURVE connections are denied.
|
||||
- GSSAPI requires no configuration.
|
||||
"""
|
||||
|
||||
context: "zmq.Context"
|
||||
encoding: str
|
||||
allow_any: bool
|
||||
credentials_providers: Dict[str, Any]
|
||||
zap_socket: "zmq.Socket"
|
||||
whitelist: Set[str]
|
||||
blacklist: Set[str]
|
||||
passwords: Dict[str, Dict[str, str]]
|
||||
certs: Dict[str, Dict[bytes, Any]]
|
||||
log: Any
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Optional["zmq.Context"] = None,
|
||||
encoding: str = 'utf-8',
|
||||
log: Any = None,
|
||||
):
|
||||
_check_version((4, 0), "security")
|
||||
self.context = context or zmq.Context.instance()
|
||||
self.encoding = encoding
|
||||
self.allow_any = False
|
||||
self.credentials_providers = {}
|
||||
self.zap_socket = None # type: ignore
|
||||
self.whitelist = set()
|
||||
self.blacklist = set()
|
||||
# passwords is a dict keyed by domain and contains values
|
||||
# of dicts with username:password pairs.
|
||||
self.passwords = {}
|
||||
# certs is dict keyed by domain and contains values
|
||||
# of dicts keyed by the public keys from the specified location.
|
||||
self.certs = {}
|
||||
self.log = log or logging.getLogger('zmq.auth')
|
||||
|
||||
def start(self) -> None:
|
||||
"""Create and bind the ZAP socket"""
|
||||
self.zap_socket = self.context.socket(zmq.REP)
|
||||
self.zap_socket.linger = 1
|
||||
self.zap_socket.bind("inproc://zeromq.zap.01")
|
||||
self.log.debug("Starting")
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Close the ZAP socket"""
|
||||
if self.zap_socket:
|
||||
self.zap_socket.close()
|
||||
self.zap_socket = None # type: ignore
|
||||
|
||||
def allow(self, *addresses: str) -> None:
|
||||
"""Allow (whitelist) IP address(es).
|
||||
|
||||
Connections from addresses not in the whitelist will be rejected.
|
||||
|
||||
- For NULL, all clients from this address will be accepted.
|
||||
- For real auth setups, they will be allowed to continue with authentication.
|
||||
|
||||
whitelist is mutually exclusive with blacklist.
|
||||
"""
|
||||
if self.blacklist:
|
||||
raise ValueError("Only use a whitelist or a blacklist, not both")
|
||||
self.log.debug("Allowing %s", ','.join(addresses))
|
||||
self.whitelist.update(addresses)
|
||||
|
||||
def deny(self, *addresses: str) -> None:
|
||||
"""Deny (blacklist) IP address(es).
|
||||
|
||||
Addresses not in the blacklist will be allowed to continue with authentication.
|
||||
|
||||
Blacklist is mutually exclusive with whitelist.
|
||||
"""
|
||||
if self.whitelist:
|
||||
raise ValueError("Only use a whitelist or a blacklist, not both")
|
||||
self.log.debug("Denying %s", ','.join(addresses))
|
||||
self.blacklist.update(addresses)
|
||||
|
||||
def configure_plain(
|
||||
self, domain: str = '*', passwords: Dict[str, str] = None
|
||||
) -> None:
|
||||
"""Configure PLAIN authentication for a given domain.
|
||||
|
||||
PLAIN authentication uses a plain-text password file.
|
||||
To cover all domains, use "*".
|
||||
You can modify the password file at any time; it is reloaded automatically.
|
||||
"""
|
||||
if passwords:
|
||||
self.passwords[domain] = passwords
|
||||
self.log.debug("Configure plain: %s", domain)
|
||||
|
||||
def configure_curve(
|
||||
self, domain: str = '*', location: Union[str, os.PathLike] = "."
|
||||
) -> None:
|
||||
"""Configure CURVE authentication for a given domain.
|
||||
|
||||
CURVE authentication uses a directory that holds all public client certificates,
|
||||
i.e. their public keys.
|
||||
|
||||
To cover all domains, use "*".
|
||||
|
||||
You can add and remove certificates in that directory at any time. configure_curve must be called
|
||||
every time certificates are added or removed, in order to update the Authenticator's state
|
||||
|
||||
To allow all client keys without checking, specify CURVE_ALLOW_ANY for the location.
|
||||
"""
|
||||
# If location is CURVE_ALLOW_ANY then allow all clients. Otherwise
|
||||
# treat location as a directory that holds the certificates.
|
||||
self.log.debug("Configure curve: %s[%s]", domain, location)
|
||||
if location == CURVE_ALLOW_ANY:
|
||||
self.allow_any = True
|
||||
else:
|
||||
self.allow_any = False
|
||||
try:
|
||||
self.certs[domain] = load_certificates(location)
|
||||
except Exception as e:
|
||||
self.log.error("Failed to load CURVE certs from %s: %s", location, e)
|
||||
|
||||
def configure_curve_callback(
|
||||
self, domain: str = '*', credentials_provider: Any = None
|
||||
) -> None:
|
||||
"""Configure CURVE authentication for a given domain.
|
||||
|
||||
CURVE authentication using a callback function validating
|
||||
the client public key according to a custom mechanism, e.g. checking the
|
||||
key against records in a db. credentials_provider is an object of a class which
|
||||
implements a callback method accepting two parameters (domain and key), e.g.::
|
||||
|
||||
class CredentialsProvider(object):
|
||||
|
||||
def __init__(self):
|
||||
...e.g. db connection
|
||||
|
||||
def callback(self, domain, key):
|
||||
valid = ...lookup key and/or domain in db
|
||||
if valid:
|
||||
logging.info('Authorizing: {0}, {1}'.format(domain, key))
|
||||
return True
|
||||
else:
|
||||
logging.warning('NOT Authorizing: {0}, {1}'.format(domain, key))
|
||||
return False
|
||||
|
||||
To cover all domains, use "*".
|
||||
|
||||
To allow all client keys without checking, specify CURVE_ALLOW_ANY for the location.
|
||||
"""
|
||||
|
||||
self.allow_any = False
|
||||
|
||||
if credentials_provider is not None:
|
||||
self.credentials_providers[domain] = credentials_provider
|
||||
else:
|
||||
self.log.error("None credentials_provider provided for domain:%s", domain)
|
||||
|
||||
def curve_user_id(self, client_public_key: bytes) -> str:
|
||||
"""Return the User-Id corresponding to a CURVE client's public key
|
||||
|
||||
Default implementation uses the z85-encoding of the public key.
|
||||
|
||||
Override to define a custom mapping of public key : user-id
|
||||
|
||||
This is only called on successful authentication.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
client_public_key: bytes
|
||||
The client public key used for the given message
|
||||
|
||||
Returns
|
||||
-------
|
||||
user_id: unicode
|
||||
The user ID as text
|
||||
"""
|
||||
return z85.encode(client_public_key).decode('ascii')
|
||||
|
||||
def configure_gssapi(
|
||||
self, domain: str = '*', location: Optional[str] = None
|
||||
) -> None:
|
||||
"""Configure GSSAPI authentication
|
||||
|
||||
Currently this is a no-op because there is nothing to configure with GSSAPI.
|
||||
"""
|
||||
|
||||
def handle_zap_message(self, msg: List[bytes]):
|
||||
"""Perform ZAP authentication"""
|
||||
if len(msg) < 6:
|
||||
self.log.error("Invalid ZAP message, not enough frames: %r", msg)
|
||||
if len(msg) < 2:
|
||||
self.log.error("Not enough information to reply")
|
||||
else:
|
||||
self._send_zap_reply(msg[1], b"400", b"Not enough frames")
|
||||
return
|
||||
|
||||
version, request_id, domain, address, identity, mechanism = msg[:6]
|
||||
credentials = msg[6:]
|
||||
|
||||
domain = domain.decode(self.encoding, 'replace')
|
||||
address = address.decode(self.encoding, 'replace')
|
||||
|
||||
if version != VERSION:
|
||||
self.log.error("Invalid ZAP version: %r", msg)
|
||||
self._send_zap_reply(request_id, b"400", b"Invalid version")
|
||||
return
|
||||
|
||||
self.log.debug(
|
||||
"version: %r, request_id: %r, domain: %r,"
|
||||
" address: %r, identity: %r, mechanism: %r",
|
||||
version,
|
||||
request_id,
|
||||
domain,
|
||||
address,
|
||||
identity,
|
||||
mechanism,
|
||||
)
|
||||
|
||||
# Is address is explicitly whitelisted or blacklisted?
|
||||
allowed = False
|
||||
denied = False
|
||||
reason = b"NO ACCESS"
|
||||
|
||||
if self.whitelist:
|
||||
if address in self.whitelist:
|
||||
allowed = True
|
||||
self.log.debug("PASSED (whitelist) address=%s", address)
|
||||
else:
|
||||
denied = True
|
||||
reason = b"Address not in whitelist"
|
||||
self.log.debug("DENIED (not in whitelist) address=%s", address)
|
||||
|
||||
elif self.blacklist:
|
||||
if address in self.blacklist:
|
||||
denied = True
|
||||
reason = b"Address is blacklisted"
|
||||
self.log.debug("DENIED (blacklist) address=%s", address)
|
||||
else:
|
||||
allowed = True
|
||||
self.log.debug("PASSED (not in blacklist) address=%s", address)
|
||||
|
||||
# Perform authentication mechanism-specific checks if necessary
|
||||
username = "anonymous"
|
||||
if not denied:
|
||||
|
||||
if mechanism == b'NULL' and not allowed:
|
||||
# For NULL, we allow if the address wasn't blacklisted
|
||||
self.log.debug("ALLOWED (NULL)")
|
||||
allowed = True
|
||||
|
||||
elif mechanism == b'PLAIN':
|
||||
# For PLAIN, even a whitelisted address must authenticate
|
||||
if len(credentials) != 2:
|
||||
self.log.error("Invalid PLAIN credentials: %r", credentials)
|
||||
self._send_zap_reply(request_id, b"400", b"Invalid credentials")
|
||||
return
|
||||
username, password = (
|
||||
c.decode(self.encoding, 'replace') for c in credentials
|
||||
)
|
||||
allowed, reason = self._authenticate_plain(domain, username, password)
|
||||
|
||||
elif mechanism == b'CURVE':
|
||||
# For CURVE, even a whitelisted address must authenticate
|
||||
if len(credentials) != 1:
|
||||
self.log.error("Invalid CURVE credentials: %r", credentials)
|
||||
self._send_zap_reply(request_id, b"400", b"Invalid credentials")
|
||||
return
|
||||
key = credentials[0]
|
||||
allowed, reason = self._authenticate_curve(domain, key)
|
||||
if allowed:
|
||||
username = self.curve_user_id(key)
|
||||
|
||||
elif mechanism == b'GSSAPI':
|
||||
if len(credentials) != 1:
|
||||
self.log.error("Invalid GSSAPI credentials: %r", credentials)
|
||||
self._send_zap_reply(request_id, b"400", b"Invalid credentials")
|
||||
return
|
||||
# use principal as user-id for now
|
||||
principal = credentials[0]
|
||||
username = principal.decode("utf8")
|
||||
allowed, reason = self._authenticate_gssapi(domain, principal)
|
||||
|
||||
if allowed:
|
||||
self._send_zap_reply(request_id, b"200", b"OK", username)
|
||||
else:
|
||||
self._send_zap_reply(request_id, b"400", reason)
|
||||
|
||||
def _authenticate_plain(
|
||||
self, domain: str, username: str, password: str
|
||||
) -> Tuple[bool, bytes]:
|
||||
"""PLAIN ZAP authentication"""
|
||||
allowed = False
|
||||
reason = b""
|
||||
if self.passwords:
|
||||
# If no domain is not specified then use the default domain
|
||||
if not domain:
|
||||
domain = '*'
|
||||
|
||||
if domain in self.passwords:
|
||||
if username in self.passwords[domain]:
|
||||
if password == self.passwords[domain][username]:
|
||||
allowed = True
|
||||
else:
|
||||
reason = b"Invalid password"
|
||||
else:
|
||||
reason = b"Invalid username"
|
||||
else:
|
||||
reason = b"Invalid domain"
|
||||
|
||||
if allowed:
|
||||
self.log.debug(
|
||||
"ALLOWED (PLAIN) domain=%s username=%s password=%s",
|
||||
domain,
|
||||
username,
|
||||
password,
|
||||
)
|
||||
else:
|
||||
self.log.debug("DENIED %s", reason)
|
||||
|
||||
else:
|
||||
reason = b"No passwords defined"
|
||||
self.log.debug("DENIED (PLAIN) %s", reason)
|
||||
|
||||
return allowed, reason
|
||||
|
||||
def _authenticate_curve(self, domain: str, client_key: bytes) -> Tuple[bool, bytes]:
|
||||
"""CURVE ZAP authentication"""
|
||||
allowed = False
|
||||
reason = b""
|
||||
if self.allow_any:
|
||||
allowed = True
|
||||
reason = b"OK"
|
||||
self.log.debug("ALLOWED (CURVE allow any client)")
|
||||
elif self.credentials_providers != {}:
|
||||
# If no explicit domain is specified then use the default domain
|
||||
if not domain:
|
||||
domain = '*'
|
||||
|
||||
if domain in self.credentials_providers:
|
||||
z85_client_key = z85.encode(client_key)
|
||||
# Callback to check if key is Allowed
|
||||
if self.credentials_providers[domain].callback(domain, z85_client_key):
|
||||
allowed = True
|
||||
reason = b"OK"
|
||||
else:
|
||||
reason = b"Unknown key"
|
||||
|
||||
status = "ALLOWED" if allowed else "DENIED"
|
||||
self.log.debug(
|
||||
"%s (CURVE auth_callback) domain=%s client_key=%s",
|
||||
status,
|
||||
domain,
|
||||
z85_client_key,
|
||||
)
|
||||
else:
|
||||
reason = b"Unknown domain"
|
||||
else:
|
||||
# If no explicit domain is specified then use the default domain
|
||||
if not domain:
|
||||
domain = '*'
|
||||
|
||||
if domain in self.certs:
|
||||
# The certs dict stores keys in z85 format, convert binary key to z85 bytes
|
||||
z85_client_key = z85.encode(client_key)
|
||||
if self.certs[domain].get(z85_client_key):
|
||||
allowed = True
|
||||
reason = b"OK"
|
||||
else:
|
||||
reason = b"Unknown key"
|
||||
|
||||
status = "ALLOWED" if allowed else "DENIED"
|
||||
self.log.debug(
|
||||
"%s (CURVE) domain=%s client_key=%s",
|
||||
status,
|
||||
domain,
|
||||
z85_client_key,
|
||||
)
|
||||
else:
|
||||
reason = b"Unknown domain"
|
||||
|
||||
return allowed, reason
|
||||
|
||||
def _authenticate_gssapi(self, domain: str, principal: bytes) -> Tuple[bool, bytes]:
|
||||
"""Nothing to do for GSSAPI, which has already been handled by an external service."""
|
||||
self.log.debug("ALLOWED (GSSAPI) domain=%s principal=%s", domain, principal)
|
||||
return True, b'OK'
|
||||
|
||||
def _send_zap_reply(
|
||||
self,
|
||||
request_id: bytes,
|
||||
status_code: bytes,
|
||||
status_text: bytes,
|
||||
user_id: str = 'anonymous',
|
||||
) -> None:
|
||||
"""Send a ZAP reply to finish the authentication."""
|
||||
user_id = user_id if status_code == b'200' else b''
|
||||
if isinstance(user_id, str):
|
||||
user_id = user_id.encode(self.encoding, 'replace')
|
||||
metadata = b'' # not currently used
|
||||
self.log.debug("ZAP reply code=%s text=%s", status_code, status_text)
|
||||
reply = [VERSION, request_id, status_code, status_text, user_id, metadata]
|
||||
self.zap_socket.send_multipart(reply)
|
||||
|
||||
|
||||
__all__ = ['Authenticator', 'CURVE_ALLOW_ANY']
|
141
.venv/Lib/site-packages/zmq/auth/certs.py
Normal file
141
.venv/Lib/site-packages/zmq/auth/certs.py
Normal file
@ -0,0 +1,141 @@
|
||||
"""0MQ authentication related functions and classes."""
|
||||
|
||||
# Copyright (C) PyZMQ Developers
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
|
||||
import datetime
|
||||
import glob
|
||||
import os
|
||||
from typing import Dict, Optional, Tuple, Union
|
||||
|
||||
import zmq
|
||||
|
||||
_cert_secret_banner = """# **** Generated on {0} by pyzmq ****
|
||||
# ZeroMQ CURVE **Secret** Certificate
|
||||
# DO NOT PROVIDE THIS FILE TO OTHER USERS nor change its permissions.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
_cert_public_banner = """# **** Generated on {0} by pyzmq ****
|
||||
# ZeroMQ CURVE Public Certificate
|
||||
# Exchange securely, or use a secure mechanism to verify the contents
|
||||
# of this file after exchange. Store public certificates in your home
|
||||
# directory, in the .curve subdirectory.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def _write_key_file(
|
||||
key_filename: Union[str, os.PathLike],
|
||||
banner: str,
|
||||
public_key: Union[str, bytes],
|
||||
secret_key: Optional[Union[str, bytes]] = None,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
encoding: str = 'utf-8',
|
||||
) -> None:
|
||||
"""Create a certificate file"""
|
||||
if isinstance(public_key, bytes):
|
||||
public_key = public_key.decode(encoding)
|
||||
if isinstance(secret_key, bytes):
|
||||
secret_key = secret_key.decode(encoding)
|
||||
with open(key_filename, 'w', encoding='utf8') as f:
|
||||
f.write(banner.format(datetime.datetime.now()))
|
||||
|
||||
f.write('metadata\n')
|
||||
if metadata:
|
||||
for k, v in metadata.items():
|
||||
if isinstance(k, bytes):
|
||||
k = k.decode(encoding)
|
||||
if isinstance(v, bytes):
|
||||
v = v.decode(encoding)
|
||||
f.write(f" {k} = {v}\n")
|
||||
|
||||
f.write('curve\n')
|
||||
f.write(f" public-key = \"{public_key}\"\n")
|
||||
|
||||
if secret_key:
|
||||
f.write(f" secret-key = \"{secret_key}\"\n")
|
||||
|
||||
|
||||
def create_certificates(
|
||||
key_dir: Union[str, os.PathLike],
|
||||
name: str,
|
||||
metadata: Optional[Dict[str, str]] = None,
|
||||
) -> Tuple[str, str]:
|
||||
"""Create zmq certificates.
|
||||
|
||||
Returns the file paths to the public and secret certificate files.
|
||||
"""
|
||||
public_key, secret_key = zmq.curve_keypair()
|
||||
base_filename = os.path.join(key_dir, name)
|
||||
secret_key_file = f"{base_filename}.key_secret"
|
||||
public_key_file = f"{base_filename}.key"
|
||||
now = datetime.datetime.now()
|
||||
|
||||
_write_key_file(public_key_file, _cert_public_banner.format(now), public_key)
|
||||
|
||||
_write_key_file(
|
||||
secret_key_file,
|
||||
_cert_secret_banner.format(now),
|
||||
public_key,
|
||||
secret_key=secret_key,
|
||||
metadata=metadata,
|
||||
)
|
||||
|
||||
return public_key_file, secret_key_file
|
||||
|
||||
|
||||
def load_certificate(
|
||||
filename: Union[str, os.PathLike]
|
||||
) -> Tuple[bytes, Optional[bytes]]:
|
||||
"""Load public and secret key from a zmq certificate.
|
||||
|
||||
Returns (public_key, secret_key)
|
||||
|
||||
If the certificate file only contains the public key,
|
||||
secret_key will be None.
|
||||
|
||||
If there is no public key found in the file, ValueError will be raised.
|
||||
"""
|
||||
public_key = None
|
||||
secret_key = None
|
||||
if not os.path.exists(filename):
|
||||
raise OSError(f"Invalid certificate file: {filename}")
|
||||
|
||||
with open(filename, 'rb') as f:
|
||||
for line in f:
|
||||
line = line.strip()
|
||||
if line.startswith(b'#'):
|
||||
continue
|
||||
if line.startswith(b'public-key'):
|
||||
public_key = line.split(b"=", 1)[1].strip(b' \t\'"')
|
||||
if line.startswith(b'secret-key'):
|
||||
secret_key = line.split(b"=", 1)[1].strip(b' \t\'"')
|
||||
if public_key and secret_key:
|
||||
break
|
||||
|
||||
if public_key is None:
|
||||
raise ValueError("No public key found in %s" % filename)
|
||||
|
||||
return public_key, secret_key
|
||||
|
||||
|
||||
def load_certificates(directory: Union[str, os.PathLike] = '.') -> Dict[bytes, bool]:
|
||||
"""Load public keys from all certificates in a directory"""
|
||||
certs = {}
|
||||
if not os.path.isdir(directory):
|
||||
raise OSError(f"Invalid certificate directory: {directory}")
|
||||
# Follow czmq pattern of public keys stored in *.key files.
|
||||
glob_string = os.path.join(directory, "*.key")
|
||||
|
||||
cert_files = glob.glob(glob_string)
|
||||
for cert_file in cert_files:
|
||||
public_key, _ = load_certificate(cert_file)
|
||||
if public_key:
|
||||
certs[public_key] = True
|
||||
return certs
|
||||
|
||||
|
||||
__all__ = ['create_certificates', 'load_certificate', 'load_certificates']
|
49
.venv/Lib/site-packages/zmq/auth/ioloop.py
Normal file
49
.venv/Lib/site-packages/zmq/auth/ioloop.py
Normal file
@ -0,0 +1,49 @@
|
||||
"""ZAP Authenticator integrated with the tornado IOLoop.
|
||||
|
||||
.. versionadded:: 14.1
|
||||
"""
|
||||
|
||||
# Copyright (C) PyZMQ Developers
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
from typing import Any, Optional
|
||||
|
||||
from tornado import ioloop
|
||||
|
||||
import zmq
|
||||
from zmq.eventloop import zmqstream
|
||||
|
||||
from .base import Authenticator
|
||||
|
||||
|
||||
class IOLoopAuthenticator(Authenticator):
|
||||
"""ZAP authentication for use in the tornado IOLoop"""
|
||||
|
||||
zap_stream: zmqstream.ZMQStream
|
||||
io_loop: ioloop.IOLoop
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Optional["zmq.Context"] = None,
|
||||
encoding: str = 'utf-8',
|
||||
log: Any = None,
|
||||
io_loop: Optional[ioloop.IOLoop] = None,
|
||||
):
|
||||
super().__init__(context, encoding, log)
|
||||
self.zap_stream = None # type: ignore
|
||||
self.io_loop = io_loop or ioloop.IOLoop.current()
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start ZAP authentication"""
|
||||
super().start()
|
||||
self.zap_stream = zmqstream.ZMQStream(self.zap_socket, self.io_loop)
|
||||
self.zap_stream.on_recv(self.handle_zap_message)
|
||||
|
||||
def stop(self):
|
||||
"""Stop ZAP authentication"""
|
||||
if self.zap_stream:
|
||||
self.zap_stream.close()
|
||||
self.zap_stream = None
|
||||
super().stop()
|
||||
|
||||
|
||||
__all__ = ['IOLoopAuthenticator']
|
259
.venv/Lib/site-packages/zmq/auth/thread.py
Normal file
259
.venv/Lib/site-packages/zmq/auth/thread.py
Normal file
@ -0,0 +1,259 @@
|
||||
"""ZAP Authenticator in a Python Thread.
|
||||
|
||||
.. versionadded:: 14.1
|
||||
"""
|
||||
|
||||
# Copyright (C) PyZMQ Developers
|
||||
# Distributed under the terms of the Modified BSD License.
|
||||
|
||||
import logging
|
||||
from itertools import chain
|
||||
from threading import Event, Thread
|
||||
from typing import Any, Dict, List, Optional, TypeVar, cast
|
||||
|
||||
import zmq
|
||||
from zmq.utils import jsonapi
|
||||
|
||||
from .base import Authenticator
|
||||
|
||||
|
||||
class AuthenticationThread(Thread):
|
||||
"""A Thread for running a zmq Authenticator
|
||||
|
||||
This is run in the background by ThreadedAuthenticator
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: "zmq.Context",
|
||||
endpoint: str,
|
||||
encoding: str = 'utf-8',
|
||||
log: Any = None,
|
||||
authenticator: Optional[Authenticator] = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.context = context or zmq.Context.instance()
|
||||
self.encoding = encoding
|
||||
self.log = log = log or logging.getLogger('zmq.auth')
|
||||
self.started = Event()
|
||||
self.authenticator: Authenticator = authenticator or Authenticator(
|
||||
context, encoding=encoding, log=log
|
||||
)
|
||||
|
||||
# create a socket to communicate back to main thread.
|
||||
self.pipe = context.socket(zmq.PAIR)
|
||||
self.pipe.linger = 1
|
||||
self.pipe.connect(endpoint)
|
||||
|
||||
def run(self) -> None:
|
||||
"""Start the Authentication Agent thread task"""
|
||||
self.authenticator.start()
|
||||
self.started.set()
|
||||
zap = self.authenticator.zap_socket
|
||||
poller = zmq.Poller()
|
||||
poller.register(self.pipe, zmq.POLLIN)
|
||||
poller.register(zap, zmq.POLLIN)
|
||||
while True:
|
||||
try:
|
||||
socks = dict(poller.poll())
|
||||
except zmq.ZMQError:
|
||||
break # interrupted
|
||||
|
||||
if self.pipe in socks and socks[self.pipe] == zmq.POLLIN:
|
||||
# Make sure all API requests are processed before
|
||||
# looking at the ZAP socket.
|
||||
while True:
|
||||
try:
|
||||
msg = self.pipe.recv_multipart(flags=zmq.NOBLOCK)
|
||||
except zmq.Again:
|
||||
break
|
||||
|
||||
terminate = self._handle_pipe(msg)
|
||||
if terminate:
|
||||
break
|
||||
if terminate:
|
||||
break
|
||||
|
||||
if zap in socks and socks[zap] == zmq.POLLIN:
|
||||
self._handle_zap()
|
||||
|
||||
self.pipe.close()
|
||||
self.authenticator.stop()
|
||||
|
||||
def _handle_zap(self) -> None:
|
||||
"""
|
||||
Handle a message from the ZAP socket.
|
||||
"""
|
||||
if self.authenticator.zap_socket is None:
|
||||
raise RuntimeError("ZAP socket closed")
|
||||
msg = self.authenticator.zap_socket.recv_multipart()
|
||||
if not msg:
|
||||
return
|
||||
self.authenticator.handle_zap_message(msg)
|
||||
|
||||
def _handle_pipe(self, msg: List[bytes]) -> bool:
|
||||
"""
|
||||
Handle a message from front-end API.
|
||||
"""
|
||||
terminate = False
|
||||
|
||||
if msg is None:
|
||||
terminate = True
|
||||
return terminate
|
||||
|
||||
command = msg[0]
|
||||
self.log.debug("auth received API command %r", command)
|
||||
|
||||
if command == b'ALLOW':
|
||||
addresses = [m.decode(self.encoding) for m in msg[1:]]
|
||||
try:
|
||||
self.authenticator.allow(*addresses)
|
||||
except Exception:
|
||||
self.log.exception("Failed to allow %s", addresses)
|
||||
|
||||
elif command == b'DENY':
|
||||
addresses = [m.decode(self.encoding) for m in msg[1:]]
|
||||
try:
|
||||
self.authenticator.deny(*addresses)
|
||||
except Exception:
|
||||
self.log.exception("Failed to deny %s", addresses)
|
||||
|
||||
elif command == b'PLAIN':
|
||||
domain = msg[1].decode(self.encoding)
|
||||
json_passwords = msg[2]
|
||||
passwords: Dict[str, str] = cast(
|
||||
Dict[str, str], jsonapi.loads(json_passwords)
|
||||
)
|
||||
self.authenticator.configure_plain(domain, passwords)
|
||||
|
||||
elif command == b'CURVE':
|
||||
# For now we don't do anything with domains
|
||||
domain = msg[1].decode(self.encoding)
|
||||
|
||||
# If location is CURVE_ALLOW_ANY, allow all clients. Otherwise
|
||||
# treat location as a directory that holds the certificates.
|
||||
location = msg[2].decode(self.encoding)
|
||||
self.authenticator.configure_curve(domain, location)
|
||||
|
||||
elif command == b'TERMINATE':
|
||||
terminate = True
|
||||
|
||||
else:
|
||||
self.log.error("Invalid auth command from API: %r", command)
|
||||
|
||||
return terminate
|
||||
|
||||
|
||||
T = TypeVar("T", bound=type)
|
||||
|
||||
|
||||
def _inherit_docstrings(cls: T) -> T:
|
||||
"""inherit docstrings from Authenticator, so we don't duplicate them"""
|
||||
for name, method in cls.__dict__.items():
|
||||
if name.startswith('_') or not callable(method):
|
||||
continue
|
||||
upstream_method = getattr(Authenticator, name, None)
|
||||
if not method.__doc__:
|
||||
method.__doc__ = upstream_method.__doc__
|
||||
return cls
|
||||
|
||||
|
||||
@_inherit_docstrings
|
||||
class ThreadAuthenticator:
|
||||
"""Run ZAP authentication in a background thread"""
|
||||
|
||||
context: "zmq.Context"
|
||||
log: Any
|
||||
encoding: str
|
||||
pipe: "zmq.Socket"
|
||||
pipe_endpoint: str = ''
|
||||
thread: AuthenticationThread
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
context: Optional["zmq.Context"] = None,
|
||||
encoding: str = 'utf-8',
|
||||
log: Any = None,
|
||||
):
|
||||
self.log = log
|
||||
self.encoding = encoding
|
||||
self.pipe = None # type: ignore
|
||||
self.pipe_endpoint = f"inproc://{id(self)}.inproc"
|
||||
self.thread = None # type: ignore
|
||||
self.context = context or zmq.Context.instance()
|
||||
|
||||
# proxy base Authenticator attributes
|
||||
|
||||
def __setattr__(self, key: str, value: Any):
|
||||
for obj in chain([self], self.__class__.mro()):
|
||||
if key in obj.__dict__ or (key in getattr(obj, "__annotations__", {})):
|
||||
object.__setattr__(self, key, value)
|
||||
return
|
||||
setattr(self.thread.authenticator, key, value)
|
||||
|
||||
def __getattr__(self, key: str):
|
||||
return getattr(self.thread.authenticator, key)
|
||||
|
||||
def allow(self, *addresses: str):
|
||||
self.pipe.send_multipart(
|
||||
[b'ALLOW'] + [a.encode(self.encoding) for a in addresses]
|
||||
)
|
||||
|
||||
def deny(self, *addresses: str):
|
||||
self.pipe.send_multipart(
|
||||
[b'DENY'] + [a.encode(self.encoding) for a in addresses]
|
||||
)
|
||||
|
||||
def configure_plain(
|
||||
self, domain: str = '*', passwords: Optional[Dict[str, str]] = None
|
||||
):
|
||||
self.pipe.send_multipart(
|
||||
[b'PLAIN', domain.encode(self.encoding), jsonapi.dumps(passwords or {})]
|
||||
)
|
||||
|
||||
def configure_curve(self, domain: str = '*', location: str = ''):
|
||||
domain = domain.encode(self.encoding)
|
||||
location = location.encode(self.encoding)
|
||||
self.pipe.send_multipart([b'CURVE', domain, location])
|
||||
|
||||
def configure_curve_callback(
|
||||
self, domain: str = '*', credentials_provider: Any = None
|
||||
):
|
||||
self.thread.authenticator.configure_curve_callback(
|
||||
domain, credentials_provider=credentials_provider
|
||||
)
|
||||
|
||||
def start(self) -> None:
|
||||
"""Start the authentication thread"""
|
||||
# create a socket to communicate with auth thread.
|
||||
self.pipe = self.context.socket(zmq.PAIR)
|
||||
self.pipe.linger = 1
|
||||
self.pipe.bind(self.pipe_endpoint)
|
||||
self.thread = AuthenticationThread(
|
||||
self.context, self.pipe_endpoint, encoding=self.encoding, log=self.log
|
||||
)
|
||||
self.thread.start()
|
||||
if not self.thread.started.wait(timeout=10):
|
||||
raise RuntimeError("Authenticator thread failed to start")
|
||||
|
||||
def stop(self) -> None:
|
||||
"""Stop the authentication thread"""
|
||||
if self.pipe:
|
||||
self.pipe.send(b'TERMINATE')
|
||||
if self.is_alive():
|
||||
self.thread.join()
|
||||
self.thread = None # type: ignore
|
||||
self.pipe.close()
|
||||
self.pipe = None # type: ignore
|
||||
|
||||
def is_alive(self) -> bool:
|
||||
"""Is the ZAP thread currently running?"""
|
||||
if self.thread and self.thread.is_alive():
|
||||
return True
|
||||
return False
|
||||
|
||||
def __del__(self) -> None:
|
||||
self.stop()
|
||||
|
||||
|
||||
__all__ = ['ThreadAuthenticator']
|
Reference in New Issue
Block a user