"""A high-speed, production ready, thread pooled, generic WSGI server."""importmimetools# todo: use emailimportQueueimportosimportrequoted_slash=re.compile("(?i)%2F")importrfc822importsockettry:importcStringIOasStringIOexceptImportError:importStringIOimportsysimportthreadingimporttimeimporttracebackfromurllibimportunquotefromurlparseimporturlparsetry:fromOpenSSLimportSSLexceptImportError:SSL=Noneimporterrnosocket_errors_to_ignore=[]# Not all of these names will be defined for every platform.for_in("EPIPE","ETIMEDOUT","ECONNREFUSED","ECONNRESET","EHOSTDOWN","EHOSTUNREACH","WSAECONNABORTED","WSAECONNREFUSED","WSAECONNRESET","WSAENETRESET","WSAETIMEDOUT"):if_indir(errno):socket_errors_to_ignore.append(getattr(errno,_))# de-dupe the listsocket_errors_to_ignore=dict.fromkeys(socket_errors_to_ignore).keys()socket_errors_to_ignore.append("timed out")# These are lowercase because mimetools.Message uses lowercase keys.comma_separated_headers=['accept','accept-charset','accept-encoding','accept-language','accept-ranges','allow','cache-control','connection','content-encoding','content-language','expect','if-match','if-none-match','pragma','proxy-authenticate','te','trailer','transfer-encoding','upgrade','vary','via','warning','www-authenticate',]classHTTPRequest(object):def__init__(self,connection):self.connection=connectionself.rfile=self.connection.rfileself.environ=connection.environ.copy()self.ready=Falseself.started_response=Falseself.status=""self.outheaders=[]self.sent_headers=Falseself.close_connection=Falsedefparse_request(self):# HTTP/1.1 connections are persistent by default. If a client# requests a page, then idles (leaves the connection open),# then rfile.readline() will raise socket.error("timed out").# Note that it does this based on the value given to settimeout(),# and doesn't need the client to request or acknowledge the close# (although your TCP stack might suffer for it: cf Apache's history# with FIN_WAIT_2).request_line=self.rfile.readline()ifnotrequest_line:# Force self.ready = False so the connection will close.self.ready=Falsereturnserver=self.connection.servermethod,path,req_protocol=request_line.strip().split(" ",2)self.environ["REQUEST_METHOD"]=method# path may be an abs_path (including "http://host.domain.tld");scheme,location,path,params,qs,frag=urlparse(path)iffrag:self.simple_response("400 Bad Request","Illegal #fragment in Request-URI.")returnifscheme:self.environ["wsgi.url_scheme"]=schemeifparams:path=path+";"+params# Unquote the path+params (e.g. "/this%20path" -> "this path").# http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2## But note that "...a URI must be separated into its components# before the escaped characters within those components can be# safely decoded." http://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2atoms=[unquote(x)forxinquoted_slash.split(path)]path="%2F".join(atoms)ifpath=="*":# This means, of course, that the last wsgi_app (shortest path)# will always handle a URI of "*".self.environ["SCRIPT_NAME"]=""self.environ["PATH_INFO"]="*"self.wsgi_app=server.mount_points[-1][1]else:formount_point,wsgi_appinserver.mount_points:# The mount_points list should be sorted by length, descending.ifpath.startswith(mount_point):self.environ["SCRIPT_NAME"]=mount_pointself.environ["PATH_INFO"]=path[len(mount_point):]self.wsgi_app=wsgi_appbreakelse:self.simple_response("404 Not Found")return# Note that, like wsgiref and most other WSGI servers,# we unquote the path but not the query string.self.environ["QUERY_STRING"]=qs# Compare request and server HTTP protocol versions, in case our# server does not support the requested protocol. Limit our output# to min(req, server). We want the following output:# request server actual written supported response# protocol protocol response protocol feature set# a 1.0 1.0 1.0 1.0# b 1.0 1.1 1.1 1.0# c 1.1 1.0 1.0 1.0# d 1.1 1.1 1.1 1.1# Notice that, in (b), the response will be "HTTP/1.1" even though# the client only understands 1.0. RFC 2616 10.5.6 says we should# only return 505 if the _major_ version is different.rp=int(req_protocol[5]),int(req_protocol[7])sp=int(server.protocol[5]),int(server.protocol[7])ifsp[0]!=rp[0]:self.simple_response("505 HTTP Version Not Supported")return# Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol.self.environ["SERVER_PROTOCOL"]=req_protocol# set a non-standard environ entry so the WSGI app can know what# the *real* server protocol is (and what features to support).# See http://www.faqs.org/rfcs/rfc2145.html.self.environ["ACTUAL_SERVER_PROTOCOL"]=server.protocolself.response_protocol="HTTP/%s.%s"%min(rp,sp)# If the Request-URI was an absoluteURI, use its location atom.iflocation:self.environ["SERVER_NAME"]=location# then all the http headersheaders=mimetools.Message(self.rfile)self.environ.update(self.parse_headers(headers))# Persistent connection supportifself.response_protocol=="HTTP/1.1":ifheaders.getheader("Connection","")=="close":self.close_connection=Trueself.outheaders.append(("Connection","close"))else:ifheaders.getheader("Connection","")=="Keep-Alive":ifself.close_connection==False:self.outheaders.append(("Connection","Keep-Alive"))else:self.close_connection=True# Transfer-Encoding supportte=headers.getheader("Transfer-Encoding","")te=[x.strip()forxinte.split(",")ifx.strip()]ifte:whilete:enc=te.pop()ifenc.lower()=="chunked":ifnotself.decode_chunked():returnelse:self.simple_response("501 Unimplemented")self.close_connection=Truereturnelse:cl=headers.getheader("Content-length")ifmethodin("POST","PUT")andclisNone:# No Content-Length header supplied. This will hang# cgi.FieldStorage, since it cannot determine when to# stop reading from the socket. Until we handle chunked# encoding, always respond with 411 Length Required.# See http://www.cherrypy.org/ticket/493.self.simple_response("411 Length Required")return# From PEP 333:# "Servers and gateways that implement HTTP 1.1 must provide# transparent support for HTTP 1.1's "expect/continue" mechanism.# This may be done in any of several ways:# 1. Respond to requests containing an Expect: 100-continue request# with an immediate "100 Continue" response, and proceed normally.# 2. Proceed with the request normally, but provide the application# with a wsgi.input stream that will send the "100 Continue"# response if/when the application first attempts to read from# the input stream. The read request must then remain blocked# until the client responds.# 3. Wait until the client decides that the server does not support# expect/continue, and sends the request body on its own.# (This is suboptimal, and is not recommended.)## We used to do 3, but are now doing 1. Maybe we'll do 2 someday,# but it seems like it would be a big slowdown for such a rare case.ifheaders.getheader("Expect","")=="100-continue":self.simple_response(100)self.ready=Truedefparse_headers(self,headers):environ={}ct=headers.getheader("Content-type","")ifct:environ["CONTENT_TYPE"]=ctcl=headers.getheader("Content-length")or""ifcl:environ["CONTENT_LENGTH"]=cl# Must use keys() here for Python 2.3 (rfc822.Message had no __iter__).forkinheaders.keys():ifkin('transfer-encoding','content-type','content-length'):continueenvname="HTTP_"+k.upper().replace("-","_")ifkincomma_separated_headers:existing=environ.get(envname)ifexisting:environ[envname]=", ".join([existing]+headers.getheaders(k))else:environ[envname]=", ".join(headers.getheaders(k))else:environ[envname]=headers[k]returnenvirondefdecode_chunked(self):"""Decode the 'chunked' transfer coding."""cl=0data=StringIO.StringIO()whileTrue:line=self.rfile.readline().strip().split(" ",1)chunk_size=int(line.pop(0),16)ifchunk_size<=0:break## if line: chunk_extension = line[0]cl+=chunk_sizedata.write(self.rfile.read(chunk_size))crlf=self.rfile.read(2)ifcrlf!="\r\n":self.simple_response("400 Bad Request","Bad chunked transfer coding ""(expected '\\r\\n', got %r)"%crlf)returnheaders=mimetools.Message(self.rfile)self.environ.update(self.parse_headers(headers))data.seek(0)self.environ["wsgi.input"]=dataself.environ["CONTENT_LENGTH"]=str(cl)or""returnTruedefrespond(self):response=self.wsgi_app(self.environ,self.start_response)forlineinresponse:self.write(line)ifhasattr(response,"close"):response.close()if(self.readyandnotself.sent_headersandnotself.connection.server.interrupt):self.sent_headers=Trueself.send_headers()defsimple_response(self,status,msg=""):"""Write a simple response back to the client."""status=str(status)wfile=self.connection.wfilewfile.write("%s%s\r\n"%(self.connection.server.protocol,status))wfile.write("Content-Length: %s\r\n"%len(msg))ifstatus[:3]=="413"andself.response_protocol=='HTTP/1.1':# Request Entity Too Largeself.close_connection=Truewfile.write("Connection: close\r\n")wfile.write("\r\n")ifmsg:wfile.write(msg)wfile.flush()defstart_response(self,status,headers,exc_info=None):ifself.started_response:ifnotexc_info:assertFalse,"Already started response"else:try:raiseexc_info[0],exc_info[1],exc_info[2]finally:exc_info=Noneself.started_response=Trueself.status=statusself.outheaders.extend(headers)returnself.writedefwrite(self,d):ifnotself.sent_headers:self.sent_headers=Trueself.send_headers()self.connection.wfile.write(d)self.connection.wfile.flush()defsend_headers(self):hkeys=[key.lower()for(key,value)inself.outheaders]if(self.response_protocol=='HTTP/1.1'and(# Request Entity Too Large. Close conn to avoid garbage.self.status[:3]=="413"# No Content-Length. Close conn to determine transfer-length.or"content-length"notinhkeys)):if"connection"notinhkeys:self.outheaders.append(("Connection","close"))self.close_connection=Trueif"date"notinhkeys:self.outheaders.append(("Date",rfc822.formatdate()))server=self.connection.serverwfile=self.connection.wfileif"server"notinhkeys:self.outheaders.append(("Server",server.version))wfile.write(server.protocol+" "+self.status+"\r\n")try:fork,vinself.outheaders:wfile.write(k+": "+v+"\r\n")exceptTypeError:ifnotisinstance(k,str):raiseTypeError("WSGI response header key %r is not a string.")ifnotisinstance(v,str):raiseTypeError("WSGI response header value %r is not a string.")else:raisewfile.write("\r\n")wfile.flush()def_ssl_wrap_method(method):defssl_method_wrapper(self,*args,**kwargs):## print (id(self), method, args, kwargs)whileTrue:try:returnmethod(self,*args,**kwargs)except(SSL.WantReadError,SSL.WantWriteError):# Sleep and try againtime.sleep(self.ssl_retry)exceptSSL.SysCallError,e:errno=e.args[0]iferrnonotinsocket_errors_to_ignore:raisesocket.error(errno)return""exceptSSL.Error,e:ife.args==(-1,'Unexpected EOF'):return""elife.args[0][0][2]=='ssl handshake failure':return""else:raise## raise socket.timeout()returnssl_method_wrapperclassSSL_fileobject(socket._fileobject):"""Faux file object attached to a socket object."""ssl_timeout=3ssl_retry=.01close=_ssl_wrap_method(socket._fileobject.close)flush=_ssl_wrap_method(socket._fileobject.flush)write=_ssl_wrap_method(socket._fileobject.write)writelines=_ssl_wrap_method(socket._fileobject.writelines)read=_ssl_wrap_method(socket._fileobject.read)readline=_ssl_wrap_method(socket._fileobject.readline)readlines=_ssl_wrap_method(socket._fileobject.readlines)classHTTPConnection(object):rbufsize=-1wbufsize=-1RequestHandlerClass=HTTPRequestenviron={"wsgi.version":(1,0),"wsgi.url_scheme":"http","wsgi.multithread":True,"wsgi.multiprocess":False,"wsgi.run_once":False,"wsgi.errors":sys.stderr,}def__init__(self,sock,addr,server):self.socket=sockself.addr=addrself.server=server# Copy the class environ into self.self.environ=self.environ.copy()iftype(sock)issocket.socket:self.rfile=self.socket.makefile("r",self.rbufsize)self.wfile=self.socket.makefile("w",self.wbufsize)else:# Assume it's an HTTPS socket wrapperself.environ["wsgi.url_scheme"]="https"self.rfile=SSL_fileobject(sock,"r",self.rbufsize)self.wfile=SSL_fileobject(sock,"w",self.wbufsize)self.environ.update({"wsgi.input":self.rfile,"SERVER_NAME":self.server.server_name,})ifisinstance(self.server.bind_addr,basestring):# AF_UNIX. This isn't really allowed by WSGI, which doesn't# address unix domain sockets. But it's better than nothing.self.environ["SERVER_PORT"]=""else:self.environ["SERVER_PORT"]=str(self.server.bind_addr[1])# optional valuesself.environ["REMOTE_HOST"]=self.addr[0]self.environ["REMOTE_ADDR"]=self.addr[0]self.environ["REMOTE_PORT"]=str(self.addr[1])defcommunicate(self):"""Read each request and respond appropriately."""try:whileTrue:# (re)set req to None so that if something goes wrong in# the RequestHandlerClass constructor, the error doesn't# get written to the previous request.req=Nonereq=self.RequestHandlerClass(self)# This order of operations should guarantee correct pipelining.req.parse_request()ifnotreq.ready:returnreq.respond()ifreq.close_connection:returnexceptsocket.error,e:errno=e.args[0]iferrnonotinsocket_errors_to_ignore:ifreq:req.simple_response("500 Internal Server Error",format_exc())returnexcept(KeyboardInterrupt,SystemExit):raiseexcept:ifreq:req.simple_response("500 Internal Server Error",format_exc())defclose(self):self.rfile.close()self.wfile.close()self.socket.close()defformat_exc(limit=None):"""Like print_exc() but return a string. Backport for Python 2.3."""try:etype,value,tb=sys.exc_info()return''.join(traceback.format_exception(etype,value,tb,limit))finally:etype=value=tb=None_SHUTDOWNREQUEST=NoneclassWorkerThread(threading.Thread):def__init__(self,server):self.ready=Falseself.server=serverthreading.Thread.__init__(self)defrun(self):try:self.ready=TruewhileTrue:conn=self.server.requests.get()ifconnis_SHUTDOWNREQUEST:returntry:conn.communicate()finally:conn.close()except(KeyboardInterrupt,SystemExit),exc:self.server.interrupt=excclassSSLConnection:def__init__(self,*args):self._ssl_conn=SSL.Connection(*args)self._lock=threading.RLock()forfin('get_context','pending','send','write','recv','read','renegotiate','bind','listen','connect','accept','setblocking','fileno','shutdown','close','get_cipher_list','getpeername','getsockname','getsockopt','setsockopt','makefile','get_app_data','set_app_data','state_string','sock_shutdown','get_peer_certificate','want_read','want_write','set_connect_state','set_accept_state','connect_ex','sendall','settimeout'):exec"""def %s(self, *args): self._lock.acquire() try: return self._ssl_conn.%s(*args) finally: self._lock.release()"""%(f,f)classCherryPyWSGIServer(object):"""An HTTP server for WSGI. bind_addr: a (host, port) tuple if TCP sockets are desired; for UNIX sockets, supply the filename as a string. wsgi_app: the WSGI 'application callable'; multiple WSGI applications may be passed as (script_name, callable) pairs. numthreads: the number of worker threads to create (default 10). server_name: the string to set for WSGI's SERVER_NAME environ entry. Defaults to socket.gethostname(). max: the maximum number of queued requests (defaults to -1 = no limit). request_queue_size: the 'backlog' argument to socket.listen(); specifies the maximum number of queued connections (default 5). timeout: the timeout in seconds for accepted connections (default 10). """protocol="HTTP/1.1"version="CherryPy/3.0.0beta2"ready=False_interrupt=NoneConnectionClass=HTTPConnection# Paths to certificate and private key filesssl_certificate=Nonessl_private_key=Nonedef__init__(self,bind_addr,wsgi_app,numthreads=10,server_name=None,max=-1,request_queue_size=5,timeout=10):self.requests=Queue.Queue(max)ifcallable(wsgi_app):# We've been handed a single wsgi_app, in CP-2.1 style.# Assume it's mounted at "".self.mount_points=[("",wsgi_app)]else:# We've been handed a list of (mount_point, wsgi_app) tuples,# so that the server can call different wsgi_apps, and also# correctly set SCRIPT_NAME.self.mount_points=wsgi_appself.mount_points.sort()self.mount_points.reverse()self.bind_addr=bind_addrself.numthreads=numthreadsor1ifnotserver_name:server_name=socket.gethostname()self.server_name=server_nameself.request_queue_size=request_queue_sizeself._workerThreads=[]self.timeout=timeoutdefstart(self):"""Run the server forever."""# We don't have to trap KeyboardInterrupt or SystemExit here,# because cherrpy.server already does so, calling self.stop() for us.# If you're using this server with another framework, you should# trap those exceptions in whatever code block calls start().self._interrupt=Nonedefbind(family,type,proto=0):"""Create (or recreate) the actual socket object."""self.socket=socket.socket(family,type,proto)self.socket.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)ifself.ssl_certificateandself.ssl_private_key:ifSSLisNone:raiseImportError("You must install pyOpenSSL to use HTTPS.")# See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473ctx=SSL.Context(SSL.SSLv23_METHOD)ctx.use_privatekey_file(self.ssl_private_key)ctx.use_certificate_file(self.ssl_certificate)self.socket=SSLConnection(ctx,self.socket)self.socket.bind(self.bind_addr)# Select the appropriate socketifisinstance(self.bind_addr,basestring):# AF_UNIX socket# So we can reuse the socket...try:os.unlink(self.bind_addr)except:pass# So everyone can access the socket...try:os.chmod(self.bind_addr,0777)except:passinfo=[(socket.AF_UNIX,socket.SOCK_STREAM,0,"",self.bind_addr)]else:# AF_INET or AF_INET6 socket# Get the correct address family for our host (allows IPv6 addresses)host,port=self.bind_addrtry:info=socket.getaddrinfo(host,port,socket.AF_UNSPEC,socket.SOCK_STREAM)exceptsocket.gaierror:# Probably a DNS issue. Assume IPv4.info=[(socket.AF_INET,socket.SOCK_STREAM,0,"",self.bind_addr)]self.socket=Nonemsg="No socket could be created"forresininfo:af,socktype,proto,canonname,sa=restry:bind(af,socktype,proto)exceptsocket.error,msg:ifself.socket:self.socket.close()self.socket=Nonecontinuebreakifnotself.socket:raisesocket.error,msg# Timeout so KeyboardInterrupt can be caught on Win32self.socket.settimeout(1)self.socket.listen(self.request_queue_size)# Create worker threadsforiinxrange(self.numthreads):self._workerThreads.append(WorkerThread(self))forworkerinself._workerThreads:worker.setName("CP WSGIServer "+worker.getName())worker.start()forworkerinself._workerThreads:whilenotworker.ready:time.sleep(.1)self.ready=Truewhileself.ready:self.tick()ifself.interrupt:whileself.interruptisTrue:# Wait for self.stop() to completetime.sleep(0.1)raiseself.interruptdeftick(self):try:s,addr=self.socket.accept()ifnotself.ready:returnifhasattr(s,'settimeout'):s.settimeout(self.timeout)conn=self.ConnectionClass(s,addr,self)self.requests.put(conn)exceptsocket.timeout:# The only reason for the timeout in start() is so we can# notice keyboard interrupts on Win32, which don't interrupt# accept() by defaultreturnexceptsocket.error,x:msg=x.args[1]ifmsgin("Bad file descriptor","Socket operation on non-socket"):# Our socket was closed.returnifmsg=="Resource temporarily unavailable":# Just try again. See http://www.cherrypy.org/ticket/479.returnraisedef_get_interrupt(self):returnself._interruptdef_set_interrupt(self,interrupt):self._interrupt=Trueself.stop()self._interrupt=interruptinterrupt=property(_get_interrupt,_set_interrupt)defstop(self):"""Gracefully shutdown a server that is serving forever."""self.ready=Falsesock=getattr(self,"socket",None)ifsock:ifnotisinstance(self.bind_addr,basestring):# Touch our own socket to make accept() return immediately.try:host,port=sock.getsockname()[:2]exceptsocket.error,x:ifx.args[1]!="Bad file descriptor":raiseelse:forresinsocket.getaddrinfo(host,port,socket.AF_UNSPEC,socket.SOCK_STREAM):af,socktype,proto,canonname,sa=ress=Nonetry:s=socket.socket(af,socktype,proto)# See http://groups.google.com/group/cherrypy-users/# browse_frm/thread/bbfe5eb39c904fe0s.settimeout(1.0)s.connect((host,port))s.close()exceptsocket.error:ifs:s.close()ifhasattr(sock,"close"):sock.close()self.socket=None# Must shut down threads here so the code that calls# this method can know when all threads are stopped.forworkerinself._workerThreads:self.requests.put(_SHUTDOWNREQUEST)# Don't join currentThread (when stop is called inside a request).current=threading.currentThread()whileself._workerThreads:worker=self._workerThreads.pop()ifworkerisnotcurrentandworker.isAlive:try:worker.join()exceptAssertionError:pass