diff --git a/examples/server.crt b/examples/server.crt new file mode 100644 index 00000000..56e71c36 --- /dev/null +++ b/examples/server.crt @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDdDCCAlwCCQC3MfdcOMwt6DANBgkqhkiG9w0BAQUFADB8MQswCQYDVQQGEwJG +UjERMA8GA1UECBMIUGljYXJkaWUxDjAMBgNVBAcTBUNyZWlsMREwDwYDVQQKEwhn +dW5pY29ybjEVMBMGA1UEAxMMZ3VuaWNvcm4ub3JnMSAwHgYJKoZIhvcNAQkBFhF1 +c2VyQGd1bmljb3JuLm9yZzAeFw0xMjEyMTQwODI2MDJaFw0xMzEyMTQwODI2MDJa +MHwxCzAJBgNVBAYTAkZSMREwDwYDVQQIEwhQaWNhcmRpZTEOMAwGA1UEBxMFQ3Jl +aWwxETAPBgNVBAoTCGd1bmljb3JuMRUwEwYDVQQDEwxndW5pY29ybi5vcmcxIDAe +BgkqhkiG9w0BCQEWEXVzZXJAZ3VuaWNvcm4ub3JnMIIBIjANBgkqhkiG9w0BAQEF +AAOCAQ8AMIIBCgKCAQEAy9RQSiGpB+HyjMpRCEfV9M/4g7gXq/qRizxDspJujoBz +SW0d4FqMHaSRX2QOA+euhtlOYTgsvWZcyv5cvDfL1CtrNWSVBrlo7wIy5tg60Z3A +JnWT/Zxj4WIqkPwdglB1sRBsI1Fn0o6nJu4HekZedXDK6fua4lOPfsQG84EhRQKS +Mz2o7Nesk8/UMjb+5WoRmG7mxrpe0/OYlnydqzqwHUQ+I5CHl1kOhePo9ZBTFMA5 +Ece8kGQs37rFCEy92xCYHDgp+CjjyYbeBskF3o0/a88K2bt8J7uXkn4h14HjtFHq +fYnqn60cwyIx3T/uMUh6EmhKQezaw60xyIivmjH8tQIDAQABMA0GCSqGSIb3DQEB +BQUAA4IBAQAKu7kzTAqONFI1qC6mnwAixSd7ml6RtyQRiIWjg4FyTJmS2NMlqUSI +CiV1g1+pv2cy9amld8hoO17ISYFZqMoRxJgD5GuN4y1lUefFe95GHI9loubIJZlR +5KlZEvCiaAQoGvYiacf4BNkljyrwgPVM5e71dGon7jyghmV6yUaUL6+1J8BU/KYg +jz8RtMtptqkwKPKQVfuDcr/eoH6uZwPRbyfqSui8SuMz3Df6Dnx1hOtlQRJC6eNo +U9L3jkmCsbbMNBAz6iQjyFHFa9iqzwL7nvqZTryjmI5Dpn+BnT7Q5cduK+N5vt4+ +RjNVrz/l6+nR68B5GO96zUTV3/KrEmFr +-----END CERTIFICATE----- diff --git a/examples/server.key b/examples/server.key new file mode 100644 index 00000000..dcf2a923 --- /dev/null +++ b/examples/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEAy9RQSiGpB+HyjMpRCEfV9M/4g7gXq/qRizxDspJujoBzSW0d +4FqMHaSRX2QOA+euhtlOYTgsvWZcyv5cvDfL1CtrNWSVBrlo7wIy5tg60Z3AJnWT +/Zxj4WIqkPwdglB1sRBsI1Fn0o6nJu4HekZedXDK6fua4lOPfsQG84EhRQKSMz2o +7Nesk8/UMjb+5WoRmG7mxrpe0/OYlnydqzqwHUQ+I5CHl1kOhePo9ZBTFMA5Ece8 +kGQs37rFCEy92xCYHDgp+CjjyYbeBskF3o0/a88K2bt8J7uXkn4h14HjtFHqfYnq +n60cwyIx3T/uMUh6EmhKQezaw60xyIivmjH8tQIDAQABAoIBAQDFzhTc3C2daLhp +yS06S/xmyCz0JwNR8qir5qAL++8ue5ll+G61+yle2wX4/LBdOck1NE3MKye/5kbG ++HImdj9od3pjJmk5TVV4HToorE7ofZ6rtA8aX1rOruWALiq0/EA6xSUsYSPQQoAU +V4sKLqAceIly6Kk2WsE21CWqyfXvcQOtfBYmFwmPCImWecJLypeheEpz1U2EYl65 +u6b0NsXeODrLXEAEFjdb6UBJtzRtTJ/OnbDvghu9xMjlT0Pj+inoAv/ePZB8bmvH +XGhZo7dzgsDZ+eys7XnbeggUOhFImzCjO1f3pIVXWThGDgKIrpc9Evac2Q3AjTFY +NV9HBV9BAoGBAOyWq7HDgeCEu54orPAmdkO4j/HFX+262BTQoVCg4OX3Iv9A/lH4 +lpVGrFlK0qJF9jb7mjDmXP2LW0fwzyHe42DGFbZkKdfiMBuE+qoPeAV9s+SjE4H3 +l3tHoAOFUt2wITcHK4EYjoLMAgrbRNiv9t/gqiMm1oIb3fkUbpOoGG3LAoGBANyN +kLop3JfN1Kzto7gJq/tLS21joexTU+s4EJ+a4Q8KH1d47DLI+4119Ot+NWwi9X3S +sbOKZOjfrGw9+HPI64i7hD9HPSK58IUbsfVR9vPlPei45inRfi6s7+EUzKifOKZU +o1ecpOSPYQHZtDToGcQCTS0IFwMXHgrkP380We9/AoGBAI1wljyz8RVUxQWMs7bu +h4187TFRGkR5i20GPSqCw3E4CkgnhuNihkO/+JF5VeuFf+jnCgtp7PX3Nh8QLATH +x4+3XIup3goeQzxwh5rbnJlLyRxLEgKFDp6490SjlCLMhU7sjmmjUK+JXz82TzZs +HF9DZPOW6G7oUg/y0xibSd95AoGAXpDEcU3pq50xh0QNYqei+gh6uthxYScJYF2V +oxmBTjWE4riSbeQHF8xvy1k+BrOmluB0GQtJ4R+minK3yM1pUCM2vPsKl40qN6h8 +UTdnr4OnW9WLunp8o/66i8OjTNmYLJk1wCcF/IoNigGSZuztv0FNXfWOCGEtHHZp +U11bAnkCgYEAoU0sdFL3IfmxnNQ9CDmgXdJM0SpUm4ECd2jM/fRdgLelL+WislCF +gHjZw+3mplIzqQ9DMwRkjbaIxP0H92OopOBIqmShWUuzWw/Dj0L8PGe/7skcwsGD +/VLEkGzrxJwP4kokUu1kvLOqHM429JXsb8wO16iMQAB93yUZ+X8PGfQ= +-----END RSA PRIVATE KEY----- diff --git a/gunicorn/config.py b/gunicorn/config.py index ee74bd74..292f7275 100644 --- a/gunicorn/config.py +++ b/gunicorn/config.py @@ -115,6 +115,21 @@ class Config(object): logger_class.install() return logger_class + @property + def is_ssl(self): + return self.certfile or self.keyfile + + @property + def ssl_options(self): + opts = {} + if self.certfile: + opts['certfile'] = self.certfile + + if self.keyfile: + opts['keyfile'] = self.keyfile + + return opts + class SettingMeta(type): def __new__(cls, name, bases, attrs): @@ -1066,3 +1081,25 @@ class ProxyAllowFrom(Setting): desc = """\ Front-end's IPs from which allowed accept proxy requests (comma separate). """ + +class KeyFile(Setting): + name = "keyfile" + section = "Ssl" + cli = ["--keyfile"] + meta = "FILE" + validator = validate_string + default = None + desc = """\ + SSL key file + """ + +class CertFile(Setting): + name = "certfile" + section = "Ssl" + cli = ["--certfile"] + meta = "FILE" + validator = validate_string + default = None + desc = """\ + SSL certificate file + """ diff --git a/gunicorn/sock.py b/gunicorn/sock.py index 2336e241..4cbd91fd 100644 --- a/gunicorn/sock.py +++ b/gunicorn/sock.py @@ -117,6 +117,14 @@ def create_sockets(conf, log): laddr = conf.address listeners = [] + # check ssl config early to raise the error on startup + # only the certfile is needed since it can contains the keyfile + if conf.certfile and not os.path.exists(conf.certfile): + raise ValueError('certfile "%s" does not exist' % conf.certfile) + + if conf.keyfile and not os.path.exists(conf.keyfile): + raise ValueError('certfile "%s" does not exist' % conf.keyfile) + # sockets are already bound if 'GUNICORN_FD' in os.environ: fds = os.environ.pop('GUNICORN_FD').split(',') diff --git a/gunicorn/workers/async.py b/gunicorn/workers/async.py index a24b6468..85ac7854 100644 --- a/gunicorn/workers/async.py +++ b/gunicorn/workers/async.py @@ -3,11 +3,10 @@ # This file is part of gunicorn released under the MIT license. # See the NOTICE for more information. -from __future__ import with_statement - from datetime import datetime import errno import socket +import ssl import gunicorn.http as http import gunicorn.http.wsgi as wsgi @@ -47,10 +46,19 @@ class AsyncWorker(base.Worker): self.log.debug("Ignored premature client disconnection. %s", e) except StopIteration as e: self.log.debug("Closing connection. %s", e) + except ssl.SSLError: + raise # pass to next try-except level except socket.error: raise # pass to next try-except level except Exception as e: self.handle_error(req, client, addr, e) + except ssl.SSLError as e: + if e.args[0] == ssl.SSL_ERROR_EOF: + self.log.debug("ssl connection closed") + client.close() + else: + self.log.debug("Error processing SSL request.") + self.handle_error(req, client, addr, e) except socket.error as e: if e.args[0] not in (errno.EPIPE, errno.ECONNRESET): self.log.exception("Socket error processing request.") diff --git a/gunicorn/workers/geventlet.py b/gunicorn/workers/geventlet.py index 173c3ff7..d1c36595 100644 --- a/gunicorn/workers/geventlet.py +++ b/gunicorn/workers/geventlet.py @@ -31,6 +31,14 @@ class EventletWorker(AsyncWorker): def timeout_ctx(self): return eventlet.Timeout(self.cfg.keepalive or None, False) + def handle(self, listener, client, addr): + if self.cfg.is_ssl: + client = eventlet.wrap_ssl(client, server_side=True, + do_handshake_on_connect=False, + **self.cfg.ssl_options) + + super(EventletWorker, self).handle(listener, client, addr) + def run(self): acceptors = [] for sock in self.sockets: diff --git a/gunicorn/workers/ggevent.py b/gunicorn/workers/ggevent.py index 8d51a677..54d81ede 100644 --- a/gunicorn/workers/ggevent.py +++ b/gunicorn/workers/ggevent.py @@ -55,16 +55,22 @@ class GeventWorker(AsyncWorker): def run(self): servers = [] + ssl_args = {} + + if self.cfg.is_ssl: + ssl_args = dict(server_side=True, + do_handshake_on_connect=False, **self.cfg.ssl_options) + for s in self.sockets: s.setblocking(1) pool = Pool(self.worker_connections) if self.server_class is not None: server = self.server_class( s, application=self.wsgi, spawn=pool, log=self.log, - handler_class=self.wsgi_handler) + handler_class=self.wsgi_handler, **ssl_args) else: hfun = partial(self.handle, s) - server = StreamServer(s, handle=hfun, spawn=pool) + server = StreamServer(s, handle=hfun, spawn=pool, **ssl_args) server.start() servers.append(server) diff --git a/gunicorn/workers/gtornado.py b/gunicorn/workers/gtornado.py index bab8eca0..d30947f2 100644 --- a/gunicorn/workers/gtornado.py +++ b/gunicorn/workers/gtornado.py @@ -75,7 +75,8 @@ class TornadoWorker(Worker): httpserver.HTTPConnection.finish = finish sys.modules["tornado.httpserver"] = httpserver - server = tornado.httpserver.HTTPServer(app, io_loop=self.ioloop) + server = tornado.httpserver.HTTPServer(app, io_loop=self.ioloop, + ssl_options=self.cfg.ssl_options) for s in self.sockets: s.setblocking(0) if hasattr(server, "add_socket"): # tornado > 2.0 diff --git a/gunicorn/workers/sync.py b/gunicorn/workers/sync.py index bf1b70a7..61291d96 100644 --- a/gunicorn/workers/sync.py +++ b/gunicorn/workers/sync.py @@ -9,6 +9,7 @@ import errno import os import select import socket +import ssl import gunicorn.http as http import gunicorn.http.wsgi as wsgi @@ -75,6 +76,11 @@ class SyncWorker(base.Worker): def handle(self, listener, client, addr): req = None try: + if self.cfg.is_ssl: + client = ssl.wrap_socket(client, server_side=True, + do_handshake_on_connect=False, + **self.cfg.ssl_options) + parser = http.RequestParser(self.cfg, client) req = six.next(parser) self.handle_request(listener, req, client, addr) @@ -82,6 +88,13 @@ class SyncWorker(base.Worker): self.log.debug("Ignored premature client disconnection. %s", e) except StopIteration as e: self.log.debug("Closing connection. %s", e) + except ssl.SSLError as e: + if e.args[0] == ssl.SSL_ERROR_EOF: + self.log.debug("ssl connection closed") + client.close() + else: + self.log.debug("Error processing SSL request.") + self.handle_error(req, client, addr, e) except socket.error as e: if e.args[0] != errno.EPIPE: self.log.exception("Error processing request.")