mirror of
https://gitlab.sectorq.eu/jaydee/omv_backup.git
synced 2025-07-03 16:35:51 +02:00
added v3
This commit is contained in:
@ -0,0 +1,63 @@
|
||||
# This file is dual licensed under the terms of the Apache License, Version
|
||||
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
|
||||
# for complete details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from cryptography.hazmat.primitives._serialization import (
|
||||
BestAvailableEncryption,
|
||||
Encoding,
|
||||
KeySerializationEncryption,
|
||||
NoEncryption,
|
||||
ParameterFormat,
|
||||
PrivateFormat,
|
||||
PublicFormat,
|
||||
_KeySerializationEncryption,
|
||||
)
|
||||
from cryptography.hazmat.primitives.serialization.base import (
|
||||
load_der_parameters,
|
||||
load_der_private_key,
|
||||
load_der_public_key,
|
||||
load_pem_parameters,
|
||||
load_pem_private_key,
|
||||
load_pem_public_key,
|
||||
)
|
||||
from cryptography.hazmat.primitives.serialization.ssh import (
|
||||
SSHCertificate,
|
||||
SSHCertificateBuilder,
|
||||
SSHCertificateType,
|
||||
SSHCertPrivateKeyTypes,
|
||||
SSHCertPublicKeyTypes,
|
||||
SSHPrivateKeyTypes,
|
||||
SSHPublicKeyTypes,
|
||||
load_ssh_private_key,
|
||||
load_ssh_public_identity,
|
||||
load_ssh_public_key,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"BestAvailableEncryption",
|
||||
"Encoding",
|
||||
"KeySerializationEncryption",
|
||||
"NoEncryption",
|
||||
"ParameterFormat",
|
||||
"PrivateFormat",
|
||||
"PublicFormat",
|
||||
"SSHCertPrivateKeyTypes",
|
||||
"SSHCertPublicKeyTypes",
|
||||
"SSHCertificate",
|
||||
"SSHCertificateBuilder",
|
||||
"SSHCertificateType",
|
||||
"SSHPrivateKeyTypes",
|
||||
"SSHPublicKeyTypes",
|
||||
"_KeySerializationEncryption",
|
||||
"load_der_parameters",
|
||||
"load_der_private_key",
|
||||
"load_der_public_key",
|
||||
"load_pem_parameters",
|
||||
"load_pem_private_key",
|
||||
"load_pem_public_key",
|
||||
"load_ssh_private_key",
|
||||
"load_ssh_public_identity",
|
||||
"load_ssh_public_key",
|
||||
]
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -0,0 +1,14 @@
|
||||
# This file is dual licensed under the terms of the Apache License, Version
|
||||
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
|
||||
# for complete details.
|
||||
|
||||
from cryptography.hazmat.bindings._rust import openssl as rust_openssl
|
||||
|
||||
load_pem_private_key = rust_openssl.keys.load_pem_private_key
|
||||
load_der_private_key = rust_openssl.keys.load_der_private_key
|
||||
|
||||
load_pem_public_key = rust_openssl.keys.load_pem_public_key
|
||||
load_der_public_key = rust_openssl.keys.load_der_public_key
|
||||
|
||||
load_pem_parameters = rust_openssl.dh.from_pem_parameters
|
||||
load_der_parameters = rust_openssl.dh.from_der_parameters
|
@ -0,0 +1,156 @@
|
||||
# This file is dual licensed under the terms of the Apache License, Version
|
||||
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
|
||||
# for complete details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import typing
|
||||
|
||||
from cryptography import x509
|
||||
from cryptography.hazmat.bindings._rust import pkcs12 as rust_pkcs12
|
||||
from cryptography.hazmat.primitives import serialization
|
||||
from cryptography.hazmat.primitives._serialization import PBES as PBES
|
||||
from cryptography.hazmat.primitives.asymmetric import (
|
||||
dsa,
|
||||
ec,
|
||||
ed448,
|
||||
ed25519,
|
||||
rsa,
|
||||
)
|
||||
from cryptography.hazmat.primitives.asymmetric.types import PrivateKeyTypes
|
||||
|
||||
__all__ = [
|
||||
"PBES",
|
||||
"PKCS12Certificate",
|
||||
"PKCS12KeyAndCertificates",
|
||||
"PKCS12PrivateKeyTypes",
|
||||
"load_key_and_certificates",
|
||||
"load_pkcs12",
|
||||
"serialize_key_and_certificates",
|
||||
]
|
||||
|
||||
PKCS12PrivateKeyTypes = typing.Union[
|
||||
rsa.RSAPrivateKey,
|
||||
dsa.DSAPrivateKey,
|
||||
ec.EllipticCurvePrivateKey,
|
||||
ed25519.Ed25519PrivateKey,
|
||||
ed448.Ed448PrivateKey,
|
||||
]
|
||||
|
||||
|
||||
PKCS12Certificate = rust_pkcs12.PKCS12Certificate
|
||||
|
||||
|
||||
class PKCS12KeyAndCertificates:
|
||||
def __init__(
|
||||
self,
|
||||
key: PrivateKeyTypes | None,
|
||||
cert: PKCS12Certificate | None,
|
||||
additional_certs: list[PKCS12Certificate],
|
||||
):
|
||||
if key is not None and not isinstance(
|
||||
key,
|
||||
(
|
||||
rsa.RSAPrivateKey,
|
||||
dsa.DSAPrivateKey,
|
||||
ec.EllipticCurvePrivateKey,
|
||||
ed25519.Ed25519PrivateKey,
|
||||
ed448.Ed448PrivateKey,
|
||||
),
|
||||
):
|
||||
raise TypeError(
|
||||
"Key must be RSA, DSA, EllipticCurve, ED25519, or ED448"
|
||||
" private key, or None."
|
||||
)
|
||||
if cert is not None and not isinstance(cert, PKCS12Certificate):
|
||||
raise TypeError("cert must be a PKCS12Certificate object or None")
|
||||
if not all(
|
||||
isinstance(add_cert, PKCS12Certificate)
|
||||
for add_cert in additional_certs
|
||||
):
|
||||
raise TypeError(
|
||||
"all values in additional_certs must be PKCS12Certificate"
|
||||
" objects"
|
||||
)
|
||||
self._key = key
|
||||
self._cert = cert
|
||||
self._additional_certs = additional_certs
|
||||
|
||||
@property
|
||||
def key(self) -> PrivateKeyTypes | None:
|
||||
return self._key
|
||||
|
||||
@property
|
||||
def cert(self) -> PKCS12Certificate | None:
|
||||
return self._cert
|
||||
|
||||
@property
|
||||
def additional_certs(self) -> list[PKCS12Certificate]:
|
||||
return self._additional_certs
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if not isinstance(other, PKCS12KeyAndCertificates):
|
||||
return NotImplemented
|
||||
|
||||
return (
|
||||
self.key == other.key
|
||||
and self.cert == other.cert
|
||||
and self.additional_certs == other.additional_certs
|
||||
)
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.key, self.cert, tuple(self.additional_certs)))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
fmt = (
|
||||
"<PKCS12KeyAndCertificates(key={}, cert={}, additional_certs={})>"
|
||||
)
|
||||
return fmt.format(self.key, self.cert, self.additional_certs)
|
||||
|
||||
|
||||
load_key_and_certificates = rust_pkcs12.load_key_and_certificates
|
||||
load_pkcs12 = rust_pkcs12.load_pkcs12
|
||||
|
||||
|
||||
_PKCS12CATypes = typing.Union[
|
||||
x509.Certificate,
|
||||
PKCS12Certificate,
|
||||
]
|
||||
|
||||
|
||||
def serialize_key_and_certificates(
|
||||
name: bytes | None,
|
||||
key: PKCS12PrivateKeyTypes | None,
|
||||
cert: x509.Certificate | None,
|
||||
cas: typing.Iterable[_PKCS12CATypes] | None,
|
||||
encryption_algorithm: serialization.KeySerializationEncryption,
|
||||
) -> bytes:
|
||||
if key is not None and not isinstance(
|
||||
key,
|
||||
(
|
||||
rsa.RSAPrivateKey,
|
||||
dsa.DSAPrivateKey,
|
||||
ec.EllipticCurvePrivateKey,
|
||||
ed25519.Ed25519PrivateKey,
|
||||
ed448.Ed448PrivateKey,
|
||||
),
|
||||
):
|
||||
raise TypeError(
|
||||
"Key must be RSA, DSA, EllipticCurve, ED25519, or ED448"
|
||||
" private key, or None."
|
||||
)
|
||||
|
||||
if not isinstance(
|
||||
encryption_algorithm, serialization.KeySerializationEncryption
|
||||
):
|
||||
raise TypeError(
|
||||
"Key encryption algorithm must be a "
|
||||
"KeySerializationEncryption instance"
|
||||
)
|
||||
|
||||
if key is None and cert is None and not cas:
|
||||
raise ValueError("You must supply at least one of key, cert, or cas")
|
||||
|
||||
return rust_pkcs12.serialize_key_and_certificates(
|
||||
name, key, cert, cas, encryption_algorithm
|
||||
)
|
@ -0,0 +1,369 @@
|
||||
# This file is dual licensed under the terms of the Apache License, Version
|
||||
# 2.0, and the BSD License. See the LICENSE file in the root of this repository
|
||||
# for complete details.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import email.base64mime
|
||||
import email.generator
|
||||
import email.message
|
||||
import email.policy
|
||||
import io
|
||||
import typing
|
||||
|
||||
from cryptography import utils, x509
|
||||
from cryptography.exceptions import UnsupportedAlgorithm, _Reasons
|
||||
from cryptography.hazmat.bindings._rust import pkcs7 as rust_pkcs7
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import ec, padding, rsa
|
||||
from cryptography.utils import _check_byteslike
|
||||
|
||||
load_pem_pkcs7_certificates = rust_pkcs7.load_pem_pkcs7_certificates
|
||||
|
||||
load_der_pkcs7_certificates = rust_pkcs7.load_der_pkcs7_certificates
|
||||
|
||||
serialize_certificates = rust_pkcs7.serialize_certificates
|
||||
|
||||
PKCS7HashTypes = typing.Union[
|
||||
hashes.SHA224,
|
||||
hashes.SHA256,
|
||||
hashes.SHA384,
|
||||
hashes.SHA512,
|
||||
]
|
||||
|
||||
PKCS7PrivateKeyTypes = typing.Union[
|
||||
rsa.RSAPrivateKey, ec.EllipticCurvePrivateKey
|
||||
]
|
||||
|
||||
|
||||
class PKCS7Options(utils.Enum):
|
||||
Text = "Add text/plain MIME type"
|
||||
Binary = "Don't translate input data into canonical MIME format"
|
||||
DetachedSignature = "Don't embed data in the PKCS7 structure"
|
||||
NoCapabilities = "Don't embed SMIME capabilities"
|
||||
NoAttributes = "Don't embed authenticatedAttributes"
|
||||
NoCerts = "Don't embed signer certificate"
|
||||
|
||||
|
||||
class PKCS7SignatureBuilder:
|
||||
def __init__(
|
||||
self,
|
||||
data: bytes | None = None,
|
||||
signers: list[
|
||||
tuple[
|
||||
x509.Certificate,
|
||||
PKCS7PrivateKeyTypes,
|
||||
PKCS7HashTypes,
|
||||
padding.PSS | padding.PKCS1v15 | None,
|
||||
]
|
||||
] = [],
|
||||
additional_certs: list[x509.Certificate] = [],
|
||||
):
|
||||
self._data = data
|
||||
self._signers = signers
|
||||
self._additional_certs = additional_certs
|
||||
|
||||
def set_data(self, data: bytes) -> PKCS7SignatureBuilder:
|
||||
_check_byteslike("data", data)
|
||||
if self._data is not None:
|
||||
raise ValueError("data may only be set once")
|
||||
|
||||
return PKCS7SignatureBuilder(data, self._signers)
|
||||
|
||||
def add_signer(
|
||||
self,
|
||||
certificate: x509.Certificate,
|
||||
private_key: PKCS7PrivateKeyTypes,
|
||||
hash_algorithm: PKCS7HashTypes,
|
||||
*,
|
||||
rsa_padding: padding.PSS | padding.PKCS1v15 | None = None,
|
||||
) -> PKCS7SignatureBuilder:
|
||||
if not isinstance(
|
||||
hash_algorithm,
|
||||
(
|
||||
hashes.SHA224,
|
||||
hashes.SHA256,
|
||||
hashes.SHA384,
|
||||
hashes.SHA512,
|
||||
),
|
||||
):
|
||||
raise TypeError(
|
||||
"hash_algorithm must be one of hashes.SHA224, "
|
||||
"SHA256, SHA384, or SHA512"
|
||||
)
|
||||
if not isinstance(certificate, x509.Certificate):
|
||||
raise TypeError("certificate must be a x509.Certificate")
|
||||
|
||||
if not isinstance(
|
||||
private_key, (rsa.RSAPrivateKey, ec.EllipticCurvePrivateKey)
|
||||
):
|
||||
raise TypeError("Only RSA & EC keys are supported at this time.")
|
||||
|
||||
if rsa_padding is not None:
|
||||
if not isinstance(rsa_padding, (padding.PSS, padding.PKCS1v15)):
|
||||
raise TypeError("Padding must be PSS or PKCS1v15")
|
||||
if not isinstance(private_key, rsa.RSAPrivateKey):
|
||||
raise TypeError("Padding is only supported for RSA keys")
|
||||
|
||||
return PKCS7SignatureBuilder(
|
||||
self._data,
|
||||
[
|
||||
*self._signers,
|
||||
(certificate, private_key, hash_algorithm, rsa_padding),
|
||||
],
|
||||
)
|
||||
|
||||
def add_certificate(
|
||||
self, certificate: x509.Certificate
|
||||
) -> PKCS7SignatureBuilder:
|
||||
if not isinstance(certificate, x509.Certificate):
|
||||
raise TypeError("certificate must be a x509.Certificate")
|
||||
|
||||
return PKCS7SignatureBuilder(
|
||||
self._data, self._signers, [*self._additional_certs, certificate]
|
||||
)
|
||||
|
||||
def sign(
|
||||
self,
|
||||
encoding: serialization.Encoding,
|
||||
options: typing.Iterable[PKCS7Options],
|
||||
backend: typing.Any = None,
|
||||
) -> bytes:
|
||||
if len(self._signers) == 0:
|
||||
raise ValueError("Must have at least one signer")
|
||||
if self._data is None:
|
||||
raise ValueError("You must add data to sign")
|
||||
options = list(options)
|
||||
if not all(isinstance(x, PKCS7Options) for x in options):
|
||||
raise ValueError("options must be from the PKCS7Options enum")
|
||||
if encoding not in (
|
||||
serialization.Encoding.PEM,
|
||||
serialization.Encoding.DER,
|
||||
serialization.Encoding.SMIME,
|
||||
):
|
||||
raise ValueError(
|
||||
"Must be PEM, DER, or SMIME from the Encoding enum"
|
||||
)
|
||||
|
||||
# Text is a meaningless option unless it is accompanied by
|
||||
# DetachedSignature
|
||||
if (
|
||||
PKCS7Options.Text in options
|
||||
and PKCS7Options.DetachedSignature not in options
|
||||
):
|
||||
raise ValueError(
|
||||
"When passing the Text option you must also pass "
|
||||
"DetachedSignature"
|
||||
)
|
||||
|
||||
if PKCS7Options.Text in options and encoding in (
|
||||
serialization.Encoding.DER,
|
||||
serialization.Encoding.PEM,
|
||||
):
|
||||
raise ValueError(
|
||||
"The Text option is only available for SMIME serialization"
|
||||
)
|
||||
|
||||
# No attributes implies no capabilities so we'll error if you try to
|
||||
# pass both.
|
||||
if (
|
||||
PKCS7Options.NoAttributes in options
|
||||
and PKCS7Options.NoCapabilities in options
|
||||
):
|
||||
raise ValueError(
|
||||
"NoAttributes is a superset of NoCapabilities. Do not pass "
|
||||
"both values."
|
||||
)
|
||||
|
||||
return rust_pkcs7.sign_and_serialize(self, encoding, options)
|
||||
|
||||
|
||||
class PKCS7EnvelopeBuilder:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
_data: bytes | None = None,
|
||||
_recipients: list[x509.Certificate] | None = None,
|
||||
):
|
||||
from cryptography.hazmat.backends.openssl.backend import (
|
||||
backend as ossl,
|
||||
)
|
||||
|
||||
if not ossl.rsa_encryption_supported(padding=padding.PKCS1v15()):
|
||||
raise UnsupportedAlgorithm(
|
||||
"RSA with PKCS1 v1.5 padding is not supported by this version"
|
||||
" of OpenSSL.",
|
||||
_Reasons.UNSUPPORTED_PADDING,
|
||||
)
|
||||
self._data = _data
|
||||
self._recipients = _recipients if _recipients is not None else []
|
||||
|
||||
def set_data(self, data: bytes) -> PKCS7EnvelopeBuilder:
|
||||
_check_byteslike("data", data)
|
||||
if self._data is not None:
|
||||
raise ValueError("data may only be set once")
|
||||
|
||||
return PKCS7EnvelopeBuilder(_data=data, _recipients=self._recipients)
|
||||
|
||||
def add_recipient(
|
||||
self,
|
||||
certificate: x509.Certificate,
|
||||
) -> PKCS7EnvelopeBuilder:
|
||||
if not isinstance(certificate, x509.Certificate):
|
||||
raise TypeError("certificate must be a x509.Certificate")
|
||||
|
||||
if not isinstance(certificate.public_key(), rsa.RSAPublicKey):
|
||||
raise TypeError("Only RSA keys are supported at this time.")
|
||||
|
||||
return PKCS7EnvelopeBuilder(
|
||||
_data=self._data,
|
||||
_recipients=[
|
||||
*self._recipients,
|
||||
certificate,
|
||||
],
|
||||
)
|
||||
|
||||
def encrypt(
|
||||
self,
|
||||
encoding: serialization.Encoding,
|
||||
options: typing.Iterable[PKCS7Options],
|
||||
) -> bytes:
|
||||
if len(self._recipients) == 0:
|
||||
raise ValueError("Must have at least one recipient")
|
||||
if self._data is None:
|
||||
raise ValueError("You must add data to encrypt")
|
||||
options = list(options)
|
||||
if not all(isinstance(x, PKCS7Options) for x in options):
|
||||
raise ValueError("options must be from the PKCS7Options enum")
|
||||
if encoding not in (
|
||||
serialization.Encoding.PEM,
|
||||
serialization.Encoding.DER,
|
||||
serialization.Encoding.SMIME,
|
||||
):
|
||||
raise ValueError(
|
||||
"Must be PEM, DER, or SMIME from the Encoding enum"
|
||||
)
|
||||
|
||||
# Only allow options that make sense for encryption
|
||||
if any(
|
||||
opt not in [PKCS7Options.Text, PKCS7Options.Binary]
|
||||
for opt in options
|
||||
):
|
||||
raise ValueError(
|
||||
"Only the following options are supported for encryption: "
|
||||
"Text, Binary"
|
||||
)
|
||||
elif PKCS7Options.Text in options and PKCS7Options.Binary in options:
|
||||
# OpenSSL accepts both options at the same time, but ignores Text.
|
||||
# We fail defensively to avoid unexpected outputs.
|
||||
raise ValueError(
|
||||
"Cannot use Binary and Text options at the same time"
|
||||
)
|
||||
|
||||
return rust_pkcs7.encrypt_and_serialize(self, encoding, options)
|
||||
|
||||
|
||||
pkcs7_decrypt_der = rust_pkcs7.decrypt_der
|
||||
pkcs7_decrypt_pem = rust_pkcs7.decrypt_pem
|
||||
pkcs7_decrypt_smime = rust_pkcs7.decrypt_smime
|
||||
|
||||
|
||||
def _smime_signed_encode(
|
||||
data: bytes, signature: bytes, micalg: str, text_mode: bool
|
||||
) -> bytes:
|
||||
# This function works pretty hard to replicate what OpenSSL does
|
||||
# precisely. For good and for ill.
|
||||
|
||||
m = email.message.Message()
|
||||
m.add_header("MIME-Version", "1.0")
|
||||
m.add_header(
|
||||
"Content-Type",
|
||||
"multipart/signed",
|
||||
protocol="application/x-pkcs7-signature",
|
||||
micalg=micalg,
|
||||
)
|
||||
|
||||
m.preamble = "This is an S/MIME signed message\n"
|
||||
|
||||
msg_part = OpenSSLMimePart()
|
||||
msg_part.set_payload(data)
|
||||
if text_mode:
|
||||
msg_part.add_header("Content-Type", "text/plain")
|
||||
m.attach(msg_part)
|
||||
|
||||
sig_part = email.message.MIMEPart()
|
||||
sig_part.add_header(
|
||||
"Content-Type", "application/x-pkcs7-signature", name="smime.p7s"
|
||||
)
|
||||
sig_part.add_header("Content-Transfer-Encoding", "base64")
|
||||
sig_part.add_header(
|
||||
"Content-Disposition", "attachment", filename="smime.p7s"
|
||||
)
|
||||
sig_part.set_payload(
|
||||
email.base64mime.body_encode(signature, maxlinelen=65)
|
||||
)
|
||||
del sig_part["MIME-Version"]
|
||||
m.attach(sig_part)
|
||||
|
||||
fp = io.BytesIO()
|
||||
g = email.generator.BytesGenerator(
|
||||
fp,
|
||||
maxheaderlen=0,
|
||||
mangle_from_=False,
|
||||
policy=m.policy.clone(linesep="\r\n"),
|
||||
)
|
||||
g.flatten(m)
|
||||
return fp.getvalue()
|
||||
|
||||
|
||||
def _smime_enveloped_encode(data: bytes) -> bytes:
|
||||
m = email.message.Message()
|
||||
m.add_header("MIME-Version", "1.0")
|
||||
m.add_header("Content-Disposition", "attachment", filename="smime.p7m")
|
||||
m.add_header(
|
||||
"Content-Type",
|
||||
"application/pkcs7-mime",
|
||||
smime_type="enveloped-data",
|
||||
name="smime.p7m",
|
||||
)
|
||||
m.add_header("Content-Transfer-Encoding", "base64")
|
||||
|
||||
m.set_payload(email.base64mime.body_encode(data, maxlinelen=65))
|
||||
|
||||
return m.as_bytes(policy=m.policy.clone(linesep="\n", max_line_length=0))
|
||||
|
||||
|
||||
def _smime_enveloped_decode(data: bytes) -> bytes:
|
||||
m = email.message_from_bytes(data)
|
||||
if m.get_content_type() not in {
|
||||
"application/x-pkcs7-mime",
|
||||
"application/pkcs7-mime",
|
||||
}:
|
||||
raise ValueError("Not an S/MIME enveloped message")
|
||||
return bytes(m.get_payload(decode=True))
|
||||
|
||||
|
||||
def _smime_remove_text_headers(data: bytes) -> bytes:
|
||||
m = email.message_from_bytes(data)
|
||||
# Using get() instead of get_content_type() since it has None as default,
|
||||
# where the latter has "text/plain". Both methods are case-insensitive.
|
||||
content_type = m.get("content-type")
|
||||
if content_type is None:
|
||||
raise ValueError(
|
||||
"Decrypted MIME data has no 'Content-Type' header. "
|
||||
"Please remove the 'Text' option to parse it manually."
|
||||
)
|
||||
if "text/plain" not in content_type:
|
||||
raise ValueError(
|
||||
f"Decrypted MIME data content type is '{content_type}', not "
|
||||
"'text/plain'. Remove the 'Text' option to parse it manually."
|
||||
)
|
||||
return bytes(m.get_payload(decode=True))
|
||||
|
||||
|
||||
class OpenSSLMimePart(email.message.MIMEPart):
|
||||
# A MIMEPart subclass that replicates OpenSSL's behavior of not including
|
||||
# a newline if there are no headers.
|
||||
def _write_headers(self, generator) -> None:
|
||||
if list(self.raw_items()):
|
||||
generator._write_headers(self)
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user