Multiple ports and SSL

232 views
Skip to first unread message

RichardC

unread,
Sep 29, 2008, 10:58:10 PM9/29/08
to cherrypy-users
Hi,

I've started using Cherrypy (3.1) recently and have been struggling to
get CherryPy listening on multiple port so I can service both HTTP and
HTTPS requests. In the end I duplicated the code in _cpserver.py and
the code at the bottom of _cpwsgi.cp (from line 330) and made a few
changes so they did not reference _cherrypy.server to get it listening
on two ports at the same time.

Is there an easier way?

The CherryPy home page lists "Easy to run multiple HTTP servers (e.g.
on multiple ports) at once" as a feauture. Is this still the case or
was it phased out going from version 3.0 to 3.1?

Thanks

Richard

Robert Brewer

unread,
Oct 1, 2008, 1:45:01 AM10/1/08
to cherryp...@googlegroups.com

Sort of. When first making 3.0, I hadn't yet dreamed of the pub/sub
approach of the WSPBus, and I made the poor design choice of having
cherrypy.server try to be a single controller for multiple HTTP servers
in the same process. So yes, that particular design was phased out.

With the new bus in 3.1, however, it should be as simple as:

cherrypy.server2 = s = _cpserver.Server()
s.socket_port = 443
s.ssl_certificate = '/path/to/cert'
s.ssl_private_key = '/path/to/privkey'
s.subscribe()

If you would rather set the port, etc. in config, you would just need to
add a new config namespace:

cherrypy.config.namespaces['server2'] = \
lambda k, v: setattr(cherrypy.server2, k, v)

You would still need to execute some code:

cherrypy.server2 = _cpserver.Server()
cherrypy.server2.subscribe()

...but you could then at least move these lines into config:

[global]
server2.socket_port = 443
server2.ssl_certificate = '/path/to/cert'
server2.ssl_private_key = '/path/to/privkey'

Hmm. Maybe I should add an "on" property to the Server (and perhaps
other Bus plugins) that subscribes it when True and unsubscribes it when
False...


Robert Brewer
fuma...@aminus.org

Robert Brewer

unread,
Oct 2, 2008, 12:00:51 PM10/2/08
to cherryp...@googlegroups.com
Oops. I hadn't seen the extra refs in _cpwsgi_server to cherrypy.server.
So yes, it is a little more complicated than that. I'll see what I can
do to make it easier. It would be nice if the below solution worked. ;)


Robert Brewer
fuma...@aminus.org

RichardC

unread,
Oct 2, 2008, 11:59:41 PM10/2/08
to cherrypy-users
Yeah, I tried that early on. Instead of duplicating that code, I now
see that:

In _cpwsgi.py changing:

def __init__(self):
server = _cherrypy.server
sockFile = server.socket_file

to

def __init__(self,server):
sockFile = server.socket_file

and in _cpserver.py

if httpserver is None:
from cherrypy import _cpwsgi
httpserver = _cpwsgi.CPWSGIServer()
if isinstance(httpserver, basestring):

to

if httpserver is None:
from cherrypy import _cpwsgi
httpserver = _cpwsgi.CPWSGIServer(self)
if isinstance(httpserver, basestring):


Gets things working.

It doesn't take care of the reference to _cherrypy.server in
CPHTTPRequest

class CPHTTPRequest(wsgiserver.HTTPRequest):

def __init__(self, sendall, environ, wsgi_app):
s = _cherrypy.server

How to fix that without messing round in wsgiserver is not clear to
me. And it only appears to affect maximum request sizes.


Hmmm interesting. cherrypy.request.scheme is set correctly, but
cherrypy.request.base is not. base is stuck on 'https'. After putting
some print statements in, it is seems odd that scheme is correct - it
seems to get fixed up after initialising the request, but I cannot see
where.

Thanks for the feedback Robert.:-)


On Oct 3, 5:00 am, "Robert Brewer" <fuman...@aminus.org> wrote:
> Oops. I hadn't seen the extra refs in _cpwsgi_server to cherrypy.server.
> So yes, it is a little more complicated than that. I'll see what I can
> do to make it easier. It would be nice if the below solution worked. ;)
>
> Robert Brewer
> fuman...@aminus.org
> > fuman...@aminus.org

RichardC

unread,
Nov 30, 2008, 8:47:34 PM11/30/08
to cherrypy-users
I've upgraded to 3.1.1 and had to reapply my changes so I can listen
for http and https at the same time. I've figured out how to make the
connection class aware of which server it is listening on so that
cherrypy.request.scheme and cherrypy.request.base are set correctly.
With the first patch below you can create a second server for SSL with
code like this:

s = cp._cpserver.Server()
s.socket_host = '0.0.0.0'
s.socket_port = 8443
s.ssl_certificate = config.basedir+'/var/ssl/host.cert'
s.ssl_private_key = config.basedir+'/var/ssl/host.key'
#s.ssl_certificate_chain = config.basedir+'/var/ssl/ca_bundle.crt'
s.subscribe()

I bought an el cheapo "chained root" certificate, which requires
installation of a certificate chain on your ssl server. The second
patch below modifies the cherrypy wsgiserver so it can load these
certificate chains.

Patch 1:
diff -ur cherrypyorig/_cpserver.py cherrypyp1/_cpserver.py
--- cherrypyorig/_cpserver.py 2008-12-01 10:35:10.000000000 +1300
+++ cherrypyp1/_cpserver.py 2008-12-01 13:36:18.000000000 +1300
@@ -72,7 +72,7 @@
httpserver = self.instance
if httpserver is None:
from cherrypy import _cpwsgi_server
- httpserver = _cpwsgi_server.CPWSGIServer()
+ httpserver = _cpwsgi_server.CPWSGIServer(self)
if isinstance(httpserver, basestring):
httpserver = attributes(httpserver)()

diff -ur cherrypyorig/_cpwsgi_server.py cherrypyp1/_cpwsgi_server.py
--- cherrypyorig/_cpwsgi_server.py 2008-12-01 10:35:10.000000000 +1300
+++ cherrypyp1/_cpwsgi_server.py 2008-12-01 13:36:18.000000000 +1300
@@ -5,11 +5,12 @@
import cherrypy
from cherrypy import wsgiserver

+servers={}

class CPHTTPRequest(wsgiserver.HTTPRequest):

def __init__(self, sendall, environ, wsgi_app):
- s = cherrypy.server
+ s = servers[environ['BIND_ADDR']]
self.max_request_header_size = s.max_request_header_size or 0
self.max_request_body_size = s.max_request_body_size or 0
wsgiserver.HTTPRequest.__init__(self, sendall, environ,
wsgi_app)
@@ -31,14 +32,15 @@

ConnectionClass = CPHTTPConnection

- def __init__(self):
- server = cherrypy.server
+ def __init__(self,server):
sockFile = server.socket_file
if sockFile:
bind_addr = sockFile
else:
bind_addr = (server.socket_host, server.socket_port)

Patch 2:
diff -ur cherrypyp1/wsgiserver/__init__.py cherrypy/wsgiserver/
__init__.py
--- cherrypyp1/wsgiserver/__init__.py 2008-12-01 13:36:18.000000000
+1300
+++ cherrypy/wsgiserver/__init__.py 2008-12-01 14:04:59.000000000
+1300
@@ -25,7 +25,8 @@

server.ssl_certificate = <filename>
server.ssl_private_key = <filename>
-
+ server.ssl_certificate_chain = <filename> # only required for
"chained root" SSL certificates
+
if __name__ == '__main__':
try:
server.start()
@@ -1453,6 +1454,12 @@
If either of these is None (both are None by default), this
server
will not use SSL. If both are given and are valid, they will be
read
on server start and used in the SSL context for the listening
socket.
+
+ ssl_certificate_chain: the filename of CA's intermediate
certificate bundle.
+
+ This is needed for cheaper "chained root" SSL certificates, it
should be
+ left as None if not required.
+
"""

protocol = "HTTP/1.1"
@@ -1466,9 +1473,10 @@
ConnectionClass = HTTPConnection
environ = {}

- # Paths to certificate and private key files
+ # Paths to certificate, private key files and certificate chain
ssl_certificate = None
ssl_private_key = None
+ ssl_certificate_chain = None

def __init__(self, bind_addr, wsgi_app, numthreads=10,
server_name=None,
max=-1, request_queue_size=5, timeout=10,
shutdown_timeout=5):
@@ -1615,6 +1623,8 @@
# See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473
ctx = SSL.Context(SSL.SSLv23_METHOD)
ctx.use_privatekey_file(self.ssl_private_key)
+ if self.ssl_certificate_chain:
+ ctx.load_verify_locations(self.ssl_certificate_chain)
ctx.use_certificate_file(self.ssl_certificate)
self.socket = SSLConnection(ctx, self.socket)
self.populate_ssl_environ()


RichardC

unread,
Dec 1, 2008, 2:31:46 AM12/1/08
to cherrypy-users
Hmmm. I was too eager to post. There are bugs in the first of those
patches.

RichardC

unread,
Dec 2, 2008, 7:05:33 PM12/2/08
to cherrypy-users
So it turns out that if you create two instances of CherryPyWSGIServer
they will share the same environment dictionary 'environ', which
causes problem when one is for HTTP and the other is for HTTPS. This
confused the hell out of me all morning. Here is the change so they
have separate 'environ's.


diff -ru cherrypyp1/wsgiserver/__init__.py cherrypyp2/wsgiserver/
__init__.py
--- cherrypyp1/wsgiserver/__init__.py 2008-12-01 13:36:18.000000000
+1300
+++ cherrypyp2/wsgiserver/__init__.py 2008-12-03 12:56:33.000000000
+1300
@@ -1464,7 +1464,7 @@
nodelay = True

ConnectionClass = HTTPConnection
- environ = {}
+ environ = None

# Paths to certificate and private key files
ssl_certificate = None
@@ -1474,6 +1474,9 @@
max=-1, request_queue_size=5, timeout=10,
shutdown_timeout=5):
self.requests = ThreadPool(self, min=numthreads or 1,
max=max)

+ if not self.environ:
+ self.environ = {}
+
if callable(wsgi_app):
# We've been handed a single wsgi_app, in CP-2.1 style.
# Assume it's mounted at "".

Tim Roberts

unread,
Dec 2, 2008, 7:25:08 PM12/2/08
to cherryp...@googlegroups.com
RichardC wrote:
> So it turns out that if you create two instances of CherryPyWSGIServer
> they will share the same environment dictionary 'environ', which
> causes problem when one is for HTTP and the other is for HTTPS. This
> confused the hell out of me all morning. Here is the change so they
> have separate 'environ's.
>
>
> diff -ru cherrypyp1/wsgiserver/__init__.py cherrypyp2/wsgiserver/
> __init__.py
> --- cherrypyp1/wsgiserver/__init__.py 2008-12-01 13:36:18 +1300
> +++ cherrypyp2/wsgiserver/__init__.py 2008-12-03 12:56:33 +1300

> @@ -1464,7 +1464,7 @@
> nodelay = True
>
> ConnectionClass = HTTPConnection
> - environ = {}
> + environ = None
>
> # Paths to certificate and private key files
> ssl_certificate = None
> @@ -1474,6 +1474,9 @@
> max=-1, request_queue_size=5, timeout=10,
> shutdown_timeout=5):
> self.requests = ThreadPool(self, min=numthreads or 1,
> max=max)
>
> + if not self.environ:
> + self.environ = {}
> +
> if callable(wsgi_app):
> # We've been handed a single wsgi_app, in CP-2.1 style.
> # Assume it's mounted at "".
>

There's no point in keeping both initializations. You might as well
drop line 1468 altogether and make the __init__ code do this:
self.environ = {}

And, in fact, that's exactly how it used to be done. The code was
changed in rev 1677 by fumanchu. Hopefully, someone will chime in with
a discussion on the relative merits.

As an unrelated side note, remember that "{}" and "None" are both false
values as far as "if not" is concerned. If you don't need to tell the
difference, then you can just leave it as "environ = {}". If you do
need to tell the difference, then the "if" needs to be "if self.environ
is not None:".

--
Tim Roberts, ti...@probo.com
Providenza & Boekelheide, Inc.

Robert Brewer

unread,
Dec 2, 2008, 7:46:28 PM12/2/08
to cherryp...@googlegroups.com
Tim Roberts wrote:
> RichardC wrote:
> > So it turns out that if you create two instances of
> > CherryPyWSGIServer
> > they will share the same environment dictionary 'environ', which
> > causes problem when one is for HTTP and the other is for HTTPS. This
> > confused the hell out of me all morning.

Sorry about that, Richard.

> > Here is the change so they
> > have separate 'environ's.
> >
> >
> > diff -ru cherrypyp1/wsgiserver/__init__.py cherrypyp2/wsgiserver/
> > __init__.py
> > --- cherrypyp1/wsgiserver/__init__.py 2008-12-01 13:36:18
+1300
> > +++ cherrypyp2/wsgiserver/__init__.py 2008-12-03 12:56:33
+1300
> > @@ -1464,7 +1464,7 @@
> > nodelay = True
> >
> > ConnectionClass = HTTPConnection
> > - environ = {}
> > + environ = None
> >
> > # Paths to certificate and private key files
> > ssl_certificate = None
> > @@ -1474,6 +1474,9 @@
> > max=-1, request_queue_size=5, timeout=10,
> > shutdown_timeout=5):
> > self.requests = ThreadPool(self, min=numthreads or 1,
> > max=max)
> >
> > + if not self.environ:
> > + self.environ = {}

I think I'd rather allow inherited values from the class. So the change
would instead be:

@@ -1474,6 +1474,9 @@
max=-1, request_queue_size=5, timeout=10,
shutdown_timeout=5):
self.requests = ThreadPool(self, min=numthreads or 1,
max=max)

+ self.environ = self.environ.copy()

This is how HTTPConnection does it (and Request.error_pages, namespace,
etc). It can be helpful for subclass authors to be able to specify
environ entries at the class level.

> > +
> > if callable(wsgi_app):
> > # We've been handed a single wsgi_app, in CP-2.1 style.
> > # Assume it's mounted at "".
>
> There's no point in keeping both initializations. You might as well
> drop line 1468 altogether and make the __init__ code do this:
> self.environ = {}
>
> And, in fact, that's exactly how it used to be done. The code was
> changed in rev 1677 by fumanchu. Hopefully, someone will chime in
with
> a discussion on the relative merits.

It's helpful when browsing classes for the class itself to declare its
instances' attributes. For example, in many IDE's, you can type:

>>> from cherrypy import wsgiserver
>>> wsgiserver.CherryPyWSGIServer.

...and have "environ" show up in a tooltip.


Robert Brewer
fuma...@aminus.org

RichardC

unread,
Dec 3, 2008, 4:03:24 PM12/3/08
to cherrypy-users
> @@ -1474,6 +1474,9 @@
> max=-1, request_queue_size=5, timeout=10,
> shutdown_timeout=5):
> self.requests = ThreadPool(self, min=numthreads or 1,
> max=max)
> + self.environ = self.environ.copy()
>
Yes that's a much smarter way of doing it.
Reply all
Reply to author
Forward
0 new messages