"""A high-speed, production ready, thread pooled, generic WSGI server."""importmimetools# todo: use emailimportQueueimportrequoted_slash=re.compile("(?i)%2F")importrfc822importsocketimportsysimportthreadingimporttimeimporttracebackfromurllibimportunquotefromurlparseimporturlparseimporterrnosocket_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()# 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):stderr=sys.stderrbufsize=-1def__init__(self,socket,addr,server):self.socket=socketself.addr=addrself.server=serverself.environ={}self.ready=Falseself.started_response=Falseself.status=""self.outheaders=[]self.outheaderkeys=[]self.rfile=self.socket.makefile("r",self.bufsize)self.wfile=self.socket.makefile("w",self.bufsize)self.sent_headers=Falsedefparse_request(self):self.sent_headers=Falseself.environ={}self.environ["wsgi.version"]=(1,0)self.environ["wsgi.url_scheme"]="http"self.environ["wsgi.input"]=self.rfileself.environ["wsgi.errors"]=self.stderrself.environ["wsgi.multithread"]=Trueself.environ["wsgi.multiprocess"]=Falseself.environ["wsgi.run_once"]=Falserequest_line=self.rfile.readline()ifnotrequest_line:self.ready=Falsereturnmethod,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)ifscheme: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)self.wsgi_app=self.server.appself.environ['SCRIPT_NAME']=''self.environ['PATH_INFO']=path# 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 (SERVER_PROTOCOL)# 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(self.server.protocol[5]),int(self.server.protocol[7])ifsp[0]!=rp[0]:self.abort("505 HTTP Version Not Supported")returnself.environ["SERVER_PROTOCOL"]="HTTP/%s.%s"%min(rp,sp)# If the Request-URI was an absoluteURI, use its location atom.self.environ["SERVER_NAME"]=locationorself.server.server_nameifisinstance(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])# then all the http headersheaders=mimetools.Message(self.rfile)self.environ["CONTENT_TYPE"]=headers.getheader("Content-type","")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.abort("411 Length Required")returnself.environ["CONTENT_LENGTH"]=clor""forkinheaders:envname="HTTP_"+k.upper().replace("-","_")ifkincomma_separated_headers:self.environ[envname]=", ".join(headers.getheaders(k))else:self.environ[envname]=headers[k]self.ready=Truedefabort(self,status,msg=""):"""Write a simple error message back to the client."""self.wfile.write("%s%s\r\n"%(self.server.protocol,status))self.wfile.write("Content-Length: %s\r\n\r\n"%len(msg))ifmsg:self.wfile.write(msg)self.wfile.flush()self.ready=Falsedefstart_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=headersself.outheaderkeys=[key.lower()for(key,value)inself.outheaders]returnself.writedefwrite(self,d):ifnotself.sent_headers:self.sent_headers=Trueself.send_headers()self.wfile.write(d)self.wfile.flush()defsend_headers(self):if"content-length"notinself.outheaderkeys:self.close_at_end=Trueif"date"notinself.outheaderkeys:self.outheaders.append(("Date",rfc822.formatdate()))if"server"notinself.outheaderkeys:self.outheaders.append(("Server",self.server.version))if(self.server.protocol=="HTTP/1.1"and"connection"notinself.outheaderkeys):self.outheaders.append(("Connection","close"))self.wfile.write(self.server.protocol+" "+self.status+"\r\n")for(k,v)inself.outheaders:self.wfile.write(k+": "+v+"\r\n")self.wfile.write("\r\n")self.wfile.flush()defterminate(self):ifself.readyandnotself.sent_headersandnotself.server.interrupt:self.sent_headers=Trueself.send_headers()self.rfile.close()self.wfile.close()self.socket.close()_SHUTDOWNREQUEST=NoneclassWorkerThread(threading.Thread):def__init__(self,server):self.ready=Falseself.server=serverthreading.Thread.__init__(self)defrun(self):try:self.ready=TruewhileTrue:request=self.server.requests.get()ifrequestis_SHUTDOWNREQUEST:returntry:try:request.parse_request()ifrequest.ready:response=request.wsgi_app(request.environ,request.start_response)forlineinresponse:request.write(line)ifhasattr(response,"close"):response.close()exceptsocket.error,e:errno=e.args[0]iferrnonotinsocket_errors_to_ignore:traceback.print_exc()except(KeyboardInterrupt,SystemExit),exc:self.server.interrupt=excexcept:traceback.print_exc()finally:request.terminate()except(KeyboardInterrupt,SystemExit),exc:self.server.interrupt=excclassCherryPyWSGIServer(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.0"version="CherryPy/3.0.0alpha"ready=False_interrupt=NoneRequestHandlerClass=HTTPRequestdef__init__(self,bind_addr,wsgi_app,numthreads=10,server_name=None,max=-1,request_queue_size=5,timeout=10):self.requests=Queue.Queue(max)self.app=wsgi_appself.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)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)request=self.RequestHandlerClass(s,addr,self)self.requests.put(request)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:ifx.args[1]=="Bad file descriptor":# Our socket was closedreturnraisedef_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