Make the preverify_ok value accessible

This commit is contained in:
Michael Lazar 2020-05-15 01:11:12 -04:00
parent 4570cabcaf
commit b5acfb594b
2 changed files with 46 additions and 31 deletions

View File

@ -320,21 +320,22 @@ class StaticDirectoryApplication(JetforceApplication):
@classmethod @classmethod
def add_arguments(cls, parser: argparse.ArgumentParser): def add_arguments(cls, parser: argparse.ArgumentParser):
# fmt: off # fmt: off
parser.add_argument( group = parser.add_argument_group("static file configuration")
group.add_argument(
"--dir", "--dir",
help="Root directory on the filesystem to serve", help="Root directory on the filesystem to serve",
default="/var/gemini", default="/var/gemini",
metavar="DIR", metavar="DIR",
dest="root_directory", dest="root_directory",
) )
parser.add_argument( group.add_argument(
"--cgi-dir", "--cgi-dir",
help="CGI script directory, relative to the server's root directory", help="CGI script directory, relative to the server's root directory",
default="cgi-bin", default="cgi-bin",
metavar="DIR", metavar="DIR",
dest="cgi_directory", dest="cgi_directory",
) )
parser.add_argument( group.add_argument(
"--index-file", "--index-file",
help="If a directory contains a file with this name, " help="If a directory contains a file with this name, "
"that file will be served instead of auto-generating an index page", "that file will be served instead of auto-generating an index page",
@ -400,7 +401,8 @@ class StaticDirectoryApplication(JetforceApplication):
stream to the client. stream to the client.
""" """
script_name = str(filesystem_path) script_name = str(filesystem_path)
cgi_env = environ.copy()
cgi_env = {k: v for k, v in environ.items() if k.isupper()}
cgi_env["GATEWAY_INTERFACE"] = "GCI/1.1" cgi_env["GATEWAY_INTERFACE"] = "GCI/1.1"
cgi_env["SCRIPT_NAME"] = script_name cgi_env["SCRIPT_NAME"] = script_name
@ -510,7 +512,6 @@ class GeminiProtocol(LineOnlyReceiver):
TIMESTAMP_FORMAT = "%d/%b/%Y:%H:%M:%S %z" TIMESTAMP_FORMAT = "%d/%b/%Y:%H:%M:%S %z"
client_addr: typing.Union[IPv4Address, IPv6Address] client_addr: typing.Union[IPv4Address, IPv6Address]
client_cert: typing.Optional[x509.Certificate]
connected_timestamp: time.struct_time connected_timestamp: time.struct_time
request: bytes request: bytes
url: str url: str
@ -531,11 +532,6 @@ class GeminiProtocol(LineOnlyReceiver):
self.response_size = 0 self.response_size = 0
self.response_buffer = "" self.response_buffer = ""
self.client_addr = self.transport.getPeer() self.client_addr = self.transport.getPeer()
self.client_cert = None
peer_cert = self.transport.getPeerCertificate()
if peer_cert:
self.client_cert = peer_cert.to_cryptography()
def lineReceived(self, line): def lineReceived(self, line):
""" """
@ -576,7 +572,8 @@ class GeminiProtocol(LineOnlyReceiver):
""" """
Construct a dictionary that will be passed to the application handler. Construct a dictionary that will be passed to the application handler.
Variable names conform to the CGI spec defined in RFC 3875. Variable names (mostly) conform to the CGI spec defined in RFC 3875.
The TLS variable names borrow from the GLV-1.12556 server.
""" """
url_parts = urllib.parse.urlparse(self.url) url_parts = urllib.parse.urlparse(self.url)
environ = { environ = {
@ -590,21 +587,39 @@ class GeminiProtocol(LineOnlyReceiver):
"SERVER_PORT": str(self.client_addr.port), "SERVER_PORT": str(self.client_addr.port),
"SERVER_PROTOCOL": "GEMINI", "SERVER_PROTOCOL": "GEMINI",
"SERVER_SOFTWARE": f"jetforce/{__version__}", "SERVER_SOFTWARE": f"jetforce/{__version__}",
"client_certificate": None,
} }
if self.client_cert:
# Extract useful information from the client certificate. These peer_certificate = self.transport.getPeerCertificate()
# mostly follow the naming convention from GLV-1.12556 if peer_certificate:
cert = self.client_cert cert = peer_certificate.to_cryptography()
environ["client_certificate"] = cert
# Extract useful information from the client certificate.
name_attrs = cert.subject.get_attributes_for_oid(CN) name_attrs = cert.subject.get_attributes_for_oid(CN)
common_name = name_attrs[0].value if name_attrs else "" common_name = name_attrs[0].value if name_attrs else ""
fingerprint_bytes = cert.fingerprint(hashes.SHA256()) fingerprint_bytes = cert.fingerprint(hashes.SHA256())
fingerprint = base64.b64encode(fingerprint_bytes).decode() fingerprint = base64.b64encode(fingerprint_bytes).decode()
not_before = cert.not_valid_before.strftime("%Y-%m-%dT%H:%M:%SZ") not_before = cert.not_valid_before.strftime("%Y-%m-%dT%H:%M:%SZ")
not_after = cert.not_valid_after.strftime("%Y-%m-%dT%H:%M:%SZ") not_after = cert.not_valid_after.strftime("%Y-%m-%dT%H:%M:%SZ")
conn = self.transport.getHandle()
tls_version = conn.get_protocol_version_name()
tls_cipher = conn.get_cipher_name()
# Grab the value that we stashed during the TLS handshake.
verified = getattr(conn, "preverify_ok", False)
environ.update( environ.update(
{ {
"AUTH_TYPE": "CERTIFICATE", "AUTH_TYPE": "CERTIFICATE",
"REMOTE_USER": common_name, "REMOTE_USER": common_name,
"TLS_CIPHER": tls_cipher,
"TLS_VERSION": tls_version,
"TLS_CLIENT_VERIFIED": verified,
"TLS_CLIENT_HASH": fingerprint, "TLS_CLIENT_HASH": fingerprint,
"TLS_CLIENT_NOT_BEFORE": not_before, "TLS_CLIENT_NOT_BEFORE": not_before,
"TLS_CLIENT_NOT_AFTER": not_after, "TLS_CLIENT_NOT_AFTER": not_after,
@ -746,7 +761,8 @@ class GeminiOpenSSLCertificateOptions(CertificateOptions):
library. Returning true will always allow client certificates, even if library. Returning true will always allow client certificates, even if
they are self-signed. they are self-signed.
""" """
return preverify_ok conn.preverify_ok = preverify_ok
return True
def proto_select_callback(self, conn, protocols): def proto_select_callback(self, conn, protocols):
""" """
@ -905,7 +921,7 @@ class GeminiServer(Factory):
raiseMinimumTo=TLSVersion.TLSv1_3, raiseMinimumTo=TLSVersion.TLSv1_3,
requireCertificate=False, requireCertificate=False,
fixBrokenPeers=True, fixBrokenPeers=True,
# ALPN, I may look into supporting this later # This is for ALPN, I may look into supporting this later.
acceptableProtocols=None, acceptableProtocols=None,
) )
@ -938,41 +954,42 @@ class GeminiServer(Factory):
action="version", action="version",
version="jetforce " + __version__ version="jetforce " + __version__
) )
parser.add_argument( group = parser.add_argument_group("server configuration")
group.add_argument(
"--host", "--host",
help="Server address to bind to", help="Server address to bind to",
default="127.0.0.1" default="127.0.0.1"
) )
parser.add_argument( group.add_argument(
"--port", "--port",
help="Server port to bind to", help="Server port to bind to",
type=int, type=int,
default=1965 default=1965
) )
parser.add_argument( group.add_argument(
"--hostname", "--hostname",
help="Server hostname", help="Server hostname",
default="localhost" default="localhost"
) )
parser.add_argument( group.add_argument(
"--tls-certfile", "--tls-certfile",
dest="certfile", dest="certfile",
help="Server TLS certificate file", help="Server TLS certificate file",
metavar="FILE", metavar="FILE",
) )
parser.add_argument( group.add_argument(
"--tls-keyfile", "--tls-keyfile",
dest="keyfile", dest="keyfile",
help="Server TLS private key file", help="Server TLS private key file",
metavar="FILE", metavar="FILE",
) )
parser.add_argument( group.add_argument(
"--tls-cafile", "--tls-cafile",
dest="cafile", dest="cafile",
help="A CA file to use for validating clients", help="A CA file to use for validating clients",
metavar="FILE", metavar="FILE",
) )
parser.add_argument( group.add_argument(
"--tls-capath", "--tls-capath",
dest="capath", dest="capath",
help="A directory containing CA files for validating clients", help="A directory containing CA files for validating clients",
@ -982,14 +999,12 @@ class GeminiServer(Factory):
return parser return parser
@classmethod @classmethod
def from_argparse( def from_command_line(
cls, app_class: typing.Type[JetforceApplication], reactor: ReactorBase = reactor cls, app_class: typing.Type[JetforceApplication], reactor: ReactorBase = reactor
): ):
""" """
Shortcut to parse command line arguments and build a server instance. Shortcut to parse command line arguments and build a server instance
for a class-based jetforce application.
This only works with class-based Jetforce applications that subclass
from JetforceApplication.
""" """
parser = cls.build_argument_parser() parser = cls.build_argument_parser()
app_class.add_arguments(parser) app_class.add_arguments(parser)
@ -1010,7 +1025,7 @@ def run_server() -> None:
""" """
Entry point for running the static directory server. Entry point for running the static directory server.
""" """
server = GeminiServer.from_argparse(app_class=StaticDirectoryApplication) server = GeminiServer.from_command_line(app_class=StaticDirectoryApplication)
server.run() server.run()

View File

@ -17,7 +17,7 @@ setuptools.setup(
author_email="lazar.michael22@gmail.com", author_email="lazar.michael22@gmail.com",
description="An Experimental Gemini Server", description="An Experimental Gemini Server",
install_requires=[ install_requires=[
"twisted", "twisted>=20.3.0",
"service_identity", # Used by twisted "service_identity", # Used by twisted
"idna", # Used by twisted "idna", # Used by twisted
"pyopenssl", # Used by twisted "pyopenssl", # Used by twisted