[hooks/security] let's use a connection, not a session
# copyright 2003-2012 LOGILAB S.A. (Paris, FRANCE), all rights reserved.# contact http://www.logilab.fr/ -- mailto:contact@logilab.fr## This file is part of CubicWeb.## CubicWeb is free software: you can redistribute it and/or modify it under the# terms of the GNU Lesser General Public License as published by the Free# Software Foundation, either version 2.1 of the License, or (at your option)# any later version.## CubicWeb is distributed in the hope that it will be useful, but WITHOUT# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS# FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more# details.## You should have received a copy of the GNU Lesser General Public License along# with CubicWeb. If not, see <http://www.gnu.org/licenses/>."""twisted server for CubicWeb web instances"""__docformat__="restructuredtext en"importsysimportselectimporttracebackimportthreadingfromurlparseimporturlsplit,urlunsplitfromcgiimportFieldStorage,parse_headerfromtwisted.internetimportreactor,task,threadsfromtwisted.webimporthttp,serverfromtwisted.webimportresourcefromtwisted.web.serverimportNOT_DONE_YETfromlogilab.mtconverterimportxml_escapefromlogilab.common.decoratorsimportmonkeypatchfromcubicwebimportConfigurationError,CW_EVENT_MANAGERfromcubicweb.utilsimportjson_dumpsfromcubicweb.webimportDirectResponsefromcubicweb.web.applicationimportCubicWebPublisherfromcubicweb.etwist.requestimportCubicWebTwistedRequestAdapterfromcubicweb.etwist.httpimportHTTPResponsedefstart_task(interval,func):lc=task.LoopingCall(func)# wait until interval has expired to actually start the task, else we have# to wait all tasks to be finished for the server to be actually startedlc.start(interval,now=False)defhost_prefixed_baseurl(baseurl,host):scheme,netloc,url,query,fragment=urlsplit(baseurl)netloc_domain='.'+'.'.join(netloc.split('.')[-2:])ifhost.endswith(netloc_domain):netloc=hostbaseurl=urlunsplit((scheme,netloc,url,query,fragment))returnbaseurlclassCubicWebRootResource(resource.Resource):def__init__(self,config,repo):resource.Resource.__init__(self)self.config=config# instantiate publisher here and not in init_publisher to get some# checks done before daemonization (eg versions consistency)self.appli=CubicWebPublisher(repo,config)self.base_url=config['base-url']self.https_url=config['https-url']globalMAX_POST_LENGTHMAX_POST_LENGTH=config['max-post-length']definit_publisher(self):config=self.config# when we have an in-memory repository, clean unused sessions every XX# seconds and properly shutdown the serverifconfig['repository-uri']=='inmemory://':ifconfig.pyro_enabled():# if pyro is enabled, we have to register to the pyro name# server, create a pyro daemon, and create a task to handle pyro# requestsself.pyro_daemon=self.appli.repo.pyro_register()self.pyro_listen_timeout=0.02self.appli.repo.looping_task(1,self.pyro_loop_event)ifconfig.mode!='test':reactor.addSystemEventTrigger('before','shutdown',self.shutdown_event)self.appli.repo.start_looping_tasks()self.set_url_rewriter()CW_EVENT_MANAGER.bind('after-registry-reload',self.set_url_rewriter)defstart_service(self):start_task(self.appli.session_handler.clean_sessions_interval,self.appli.session_handler.clean_sessions)defset_url_rewriter(self):self.url_rewriter=self.appli.vreg['components'].select_or_none('urlrewriter')defshutdown_event(self):"""callback fired when the server is shutting down to properly clean opened sessions """self.appli.repo.shutdown()defpyro_loop_event(self):"""listen for pyro events"""try:self.pyro_daemon.handleRequests(self.pyro_listen_timeout)exceptselect.error:returndefgetChild(self,path,request):"""Indicate which resource to use to process down the URL's path"""returnselfdefrender(self,request):"""Render a page from the root resource"""# reload modified files in debug modeifself.config.debugmode:self.config.uiprops.reload_if_needed()ifself.https_url:self.config.https_uiprops.reload_if_needed()self.appli.vreg.reload_if_needed()ifself.config['profile']:# default profiler don't trace threadsreturnself.render_request(request)else:deferred=threads.deferToThread(self.render_request,request)returnNOT_DONE_YETdefrender_request(self,request):try:# processing HUGE files (hundred of megabytes) in http.processReceived# blocks other HTTP requests processing# due to the clumsy & slow parsing algorithm of cgi.FieldStorage# so we deferred that part to the cubicweb threadrequest.process_multipart()returnself._render_request(request)exceptException:trace=traceback.format_exc()returnHTTPResponse(stream='<pre>%s</pre>'%xml_escape(trace),code=500,twisted_request=request)def_render_request(self,request):origpath=request.pathhost=request.host# dual http/https access handling: expect a rewrite rule to prepend# 'https' to the path to detect https accesshttps=Falseiforigpath.split('/',2)[1]=='https':origpath=origpath[6:]request.uri=request.uri[6:]https=Trueifself.url_rewriterisnotNone:# XXX should occur before authentication?path=self.url_rewriter.rewrite(host,origpath,request)request.uri.replace(origpath,path,1)else:path=origpathreq=CubicWebTwistedRequestAdapter(request,self.appli.vreg,https)try:### Try to generate the actual request contentcontent=self.appli.handle_request(req,path)exceptDirectResponseasex:returnex.response# at last: create twisted objectreturnHTTPResponse(code=req.status_out,headers=req.headers_out,stream=content,twisted_request=req._twreq)# these are overridden by set_log_methods below# only defining here to prevent pylint from complaining@classmethoddefdebug(cls,msg,*a,**kw):passinfo=warning=error=critical=exception=debugJSON_PATHS=set(('json',))FRAME_POST_PATHS=set(('validateform',))orig_gotLength=http.Request.gotLength@monkeypatch(http.Request)defgotLength(self,length):orig_gotLength(self,length)iflength>MAX_POST_LENGTH:# length is 0 on GETpath=self.channel._path.split('?',1)[0].rstrip('/').rsplit('/',1)[-1]self.clientproto='HTTP/1.1'# not yet initializedself.channel.persistent=0# force connection close on cleanupself.setResponseCode(http.BAD_REQUEST)ifpathinJSON_PATHS:# XXX better json path detectionself.setHeader('content-type',"application/json")body=json_dumps({'reason':'request max size exceeded'})elifpathinFRAME_POST_PATHS:# XXX better frame post path detectionself.setHeader('content-type',"text/html")body=('<script type="text/javascript">''window.parent.handleFormValidationResponse(null, null, null, %s, null);''</script>'%json_dumps((False,'request max size exceeded',None)))else:self.setHeader('content-type',"text/html")body=("<html><head><title>Processing Failed</title></head><body>""<b>request max size exceeded</b></body></html>")self.setHeader('content-length',str(len(body)))self.write(body)# see request.finish(). Done here since we get error due to not full# initialized requestself.finished=1ifnotself.queued:self._cleanup()fordinself.notifications:d.callback(None)self.notifications=[]@monkeypatch(http.Request)defrequestReceived(self,command,path,version):"""Called by channel when all data has been received. This method is not intended for users. """self.content.seek(0,0)self.args={}self.files={}self.stack=[]self.method,self.uri=command,pathself.clientproto=versionx=self.uri.split('?',1)iflen(x)==1:self.path=self.urielse:self.path,argstring=xself.args=http.parse_qs(argstring,1)# cache the client and server information, we'll need this later to be# serialized and sent with the request so CGIs will work remotelyself.client=self.channel.transport.getPeer()self.host=self.channel.transport.getHost()# Argument processingctype=self.getHeader('content-type')self._do_process_multipart=Falseifself.method=="POST"andctype:key,pdict=parse_header(ctype)ifkey=='application/x-www-form-urlencoded':self.args.update(http.parse_qs(self.content.read(),1))self.content.seek(0)elifkey=='multipart/form-data':# defer this as it can be extremely time consumming# with big filesself._do_process_multipart=Trueself.process()@monkeypatch(http.Request)defprocess_multipart(self):ifnotself._do_process_multipart:returnform=FieldStorage(self.content,self.received_headers,environ={'REQUEST_METHOD':'POST'},keep_blank_values=1,strict_parsing=1)forkeyinform:values=form[key]ifnotisinstance(values,list):values=[values]forvalueinvalues:ifvalue.filename:ifvalue.done!=-1:# -1 is transfer has been interruptedself.files.setdefault(key,[]).append((value.filename,value.file))else:self.files.setdefault(key,[]).append((None,None))else:self.args.setdefault(key,[]).append(value.value)fromloggingimportgetLoggerfromcubicwebimportset_log_methodsLOGGER=getLogger('cubicweb.twisted')set_log_methods(CubicWebRootResource,LOGGER)defrun(config,debug=None,repo=None):# repo may by passed during test.## Test has already created a repo object so we should not create a new one.# Explicitly passing the repo object avoid relying on the fragile# config.repository() cache. We could imagine making repo a mandatory# argument and receives it from the starting command directly.ifdebugisnotNone:config.debugmode=debugconfig.check_writeable_uid_directory(config.appdatahome)# create the siteifrepoisNone:repo=config.repository()root_resource=CubicWebRootResource(config,repo)website=server.Site(root_resource)# serve it via standard HTTP on port set in the configurationport=config['port']or8080interface=config['interface']reactor.suggestThreadPoolSize(config['webserver-threadpool-size'])reactor.listenTCP(port,website,interface=interface)ifnotconfig.debugmode:ifsys.platform=='win32':raiseConfigurationError("Under windows, you must use the service management ""commands (e.g : 'net start my_instance)'")fromlogilab.common.daemonimportdaemonizeLOGGER.info('instance started in the background on %s',root_resource.base_url)whichproc=daemonize(config['pid-file'],umask=config['umask'])ifwhichproc:# 1 = orig process, 2 = first fork, None = second fork (eg daemon process)returnwhichproc# parent processroot_resource.init_publisher()# before changing uidifconfig['uid']isnotNone:fromlogilab.common.daemonimportsetugidsetugid(config['uid'])root_resource.start_service()LOGGER.info('instance started on %s',root_resource.base_url)# avoid annoying warnign if not in Main Threadsignals=threading.currentThread().getName()=='MainThread'ifconfig['profile']:importcProfilecProfile.runctx('reactor.run(installSignalHandlers=%s)'%signals,globals(),locals(),config['profile'])else:reactor.run(installSignalHandlers=signals)