From 2264d79d028c5192205b7141e1251364a2085006 Mon Sep 17 00:00:00 2001 From: sam Date: Thu, 26 Apr 2012 16:38:55 +0700 Subject: [PATCH 1/6] Now with encryption and compression --- src/paste.py | 18 +-- start.py | 22 +++- static/js/behavior.js | 37 +++++- static/js/jquery-1.7.2.min.js | 4 + static/js/lzw.js | 236 ++++++++++++++++++++++++++++++++++ views/base.tpl | 12 +- views/home.tpl | 68 ++++------ views/paste.tpl | 60 +++++++++ 8 files changed, 395 insertions(+), 62 deletions(-) create mode 100644 static/js/jquery-1.7.2.min.js create mode 100644 static/js/lzw.js diff --git a/src/paste.py b/src/paste.py index 15d33b3..4c765e5 100644 --- a/src/paste.py +++ b/src/paste.py @@ -26,24 +26,24 @@ class Paste(object): def __init__(self, uuid=None, content=None, - expire_in=u'burn_after_reading', + expiration=u'burn_after_reading', comments=None): self.content = content.encode('utf8') self.uuid = uuid or hashlib.sha1(self.content).hexdigest() - self.expire_in = self.get_expiration(expire_in) + self.expiration = self.get_expiration(expiration) self.comments = comments - def get_expiration(self, expire_in): + def get_expiration(self, expiration): """ Return a tuple with the date at which the Paste will expire or if it should be destroyed after first reading. """ try: - return datetime.now() + timedelta(seconds=self.DURATIONS[expire_in]) + return datetime.now() + timedelta(seconds=self.DURATIONS[expiration]) except KeyError: return u'burn_after_reading' @@ -82,19 +82,19 @@ class Paste(object): try: paste = open(path) uuid = os.path.basename(path) - expire_in = paste.next() + expiration = paste.next() data = paste.next() comments = paste.read()[:-1] # remove the last coma - if expire_in != u'burn_after_reading': - expire_in = datetime.strptime(expire_in, '%Y-%m-%d %H:%M:%S.%f') + if expiration != u'burn_after_reading': + expiration = datetime.strptime(expiration, '%Y-%m-%d %H:%M:%S.%f') except StopIteration: raise TypeError(u'File %s is malformed' % path) except (IOError, OSError): raise ValueError(u'Can not open paste from file %s' % path) - return Paste(uuid=uuid, comments=comments, expire_in=expire_in, **data) + return Paste(uuid=uuid, comments=comments, expiration=expiration, **data) @classmethod @@ -136,7 +136,7 @@ class Paste(object): self.DIR_CACHE.add((head, tail)) with open(self.path, 'w') as f: - f.write(unicode(self.expire_in) + '\n') + f.write(unicode(self.expiration) + '\n') f.write(self.content + '\n') if self.comments: f.write(comments) diff --git a/start.py b/start.py index 76fedd4..617b010 100644 --- a/start.py +++ b/start.py @@ -31,8 +31,26 @@ def create_paste(): content = u'' if content: - expire_in = request.forms.get('expire_in', u'burn_after_reading') - paste = Paste(expire_in=expire_in, content=content) + expiration = request.forms.get('expiration', u'burn_after_reading') + paste = Paste(expiration=expiration, content=content) + paste.save() + + return paste.uuid + + return '' + + +@app.route('/paste/') +def display_paste(paste_id): + + try: + paste = Paste.load(paste_id) + except (TypeError, ValueError): + return '' + + if content: + expiration = request.forms.get('expiration', u'burn_after_reading') + paste = Paste(expiration=expiration, content=content) paste.save() return paste.uuid diff --git a/static/js/behavior.js b/static/js/behavior.js index 8bea295..02c94fd 100644 --- a/static/js/behavior.js +++ b/static/js/behavior.js @@ -1,11 +1,46 @@ - +; // Start random number generator seeding ASAP sjcl.random.startCollectors(); $(function(){ +function encrypt(key, content) { + content = lzw.compress(sjcl.encrypt(key, content)); + content = sjcl.codec.utf8String.toBits(content); + return sjcl.codec.base64.fromBits(content); +} +function decrypt(key, content) { + content = sjcl.codec.base64.toBits(content); + content = sjcl.codec.utf8String.fromBits(content); + return sjcl.decrypt(key, lzw.decompress(content)); +} +function make_key() { + return sjcl.codec.base64.fromBits(sjcl.random.randomWords(8, 0), 0); +} + +$('button[type=submit]').click(function(e){ + + e.preventDefault(); + var paste = $('textarea').val(); + + if (paste.trim()) { + var expiration = $('#expiration').val(); + var key = make_key(); + var data = {content: encrypt(key, paste), expiration: expiration} + + $.post('/paste/create', data) + .error(function() { + alert('Paste could not be saved. Please try again later.'); + }) + .success(function(data) { + alert('success'); + window.location = '/paste/' + data + '#' + key; + }); + } + +}); }); \ No newline at end of file diff --git a/static/js/jquery-1.7.2.min.js b/static/js/jquery-1.7.2.min.js new file mode 100644 index 0000000..16ad06c --- /dev/null +++ b/static/js/jquery-1.7.2.min.js @@ -0,0 +1,4 @@ +/*! jQuery v1.7.2 jquery.com | jquery.org/license */ +(function(a,b){function cy(a){return f.isWindow(a)?a:a.nodeType===9?a.defaultView||a.parentWindow:!1}function cu(a){if(!cj[a]){var b=c.body,d=f("<"+a+">").appendTo(b),e=d.css("display");d.remove();if(e==="none"||e===""){ck||(ck=c.createElement("iframe"),ck.frameBorder=ck.width=ck.height=0),b.appendChild(ck);if(!cl||!ck.createElement)cl=(ck.contentWindow||ck.contentDocument).document,cl.write((f.support.boxModel?"":"")+""),cl.close();d=cl.createElement(a),cl.body.appendChild(d),e=f.css(d,"display"),b.removeChild(ck)}cj[a]=e}return cj[a]}function ct(a,b){var c={};f.each(cp.concat.apply([],cp.slice(0,b)),function(){c[this]=a});return c}function cs(){cq=b}function cr(){setTimeout(cs,0);return cq=f.now()}function ci(){try{return new a.ActiveXObject("Microsoft.XMLHTTP")}catch(b){}}function ch(){try{return new a.XMLHttpRequest}catch(b){}}function cb(a,c){a.dataFilter&&(c=a.dataFilter(c,a.dataType));var d=a.dataTypes,e={},g,h,i=d.length,j,k=d[0],l,m,n,o,p;for(g=1;g0){if(c!=="border")for(;e=0===c})}function S(a){return!a||!a.parentNode||a.parentNode.nodeType===11}function K(){return!0}function J(){return!1}function n(a,b,c){var d=b+"defer",e=b+"queue",g=b+"mark",h=f._data(a,d);h&&(c==="queue"||!f._data(a,e))&&(c==="mark"||!f._data(a,g))&&setTimeout(function(){!f._data(a,e)&&!f._data(a,g)&&(f.removeData(a,d,!0),h.fire())},0)}function m(a){for(var b in a){if(b==="data"&&f.isEmptyObject(a[b]))continue;if(b!=="toJSON")return!1}return!0}function l(a,c,d){if(d===b&&a.nodeType===1){var e="data-"+c.replace(k,"-$1").toLowerCase();d=a.getAttribute(e);if(typeof d=="string"){try{d=d==="true"?!0:d==="false"?!1:d==="null"?null:f.isNumeric(d)?+d:j.test(d)?f.parseJSON(d):d}catch(g){}f.data(a,c,d)}else d=b}return d}function h(a){var b=g[a]={},c,d;a=a.split(/\s+/);for(c=0,d=a.length;c)[^>]*$|#([\w\-]*)$)/,j=/\S/,k=/^\s+/,l=/\s+$/,m=/^<(\w+)\s*\/?>(?:<\/\1>)?$/,n=/^[\],:{}\s]*$/,o=/\\(?:["\\\/bfnrt]|u[0-9a-fA-F]{4})/g,p=/"[^"\\\n\r]*"|true|false|null|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?/g,q=/(?:^|:|,)(?:\s*\[)+/g,r=/(webkit)[ \/]([\w.]+)/,s=/(opera)(?:.*version)?[ \/]([\w.]+)/,t=/(msie) ([\w.]+)/,u=/(mozilla)(?:.*? rv:([\w.]+))?/,v=/-([a-z]|[0-9])/ig,w=/^-ms-/,x=function(a,b){return(b+"").toUpperCase()},y=d.userAgent,z,A,B,C=Object.prototype.toString,D=Object.prototype.hasOwnProperty,E=Array.prototype.push,F=Array.prototype.slice,G=String.prototype.trim,H=Array.prototype.indexOf,I={};e.fn=e.prototype={constructor:e,init:function(a,d,f){var g,h,j,k;if(!a)return this;if(a.nodeType){this.context=this[0]=a,this.length=1;return this}if(a==="body"&&!d&&c.body){this.context=c,this[0]=c.body,this.selector=a,this.length=1;return this}if(typeof a=="string"){a.charAt(0)!=="<"||a.charAt(a.length-1)!==">"||a.length<3?g=i.exec(a):g=[null,a,null];if(g&&(g[1]||!d)){if(g[1]){d=d instanceof e?d[0]:d,k=d?d.ownerDocument||d:c,j=m.exec(a),j?e.isPlainObject(d)?(a=[c.createElement(j[1])],e.fn.attr.call(a,d,!0)):a=[k.createElement(j[1])]:(j=e.buildFragment([g[1]],[k]),a=(j.cacheable?e.clone(j.fragment):j.fragment).childNodes);return e.merge(this,a)}h=c.getElementById(g[2]);if(h&&h.parentNode){if(h.id!==g[2])return f.find(a);this.length=1,this[0]=h}this.context=c,this.selector=a;return this}return!d||d.jquery?(d||f).find(a):this.constructor(d).find(a)}if(e.isFunction(a))return f.ready(a);a.selector!==b&&(this.selector=a.selector,this.context=a.context);return e.makeArray(a,this)},selector:"",jquery:"1.7.2",length:0,size:function(){return this.length},toArray:function(){return F.call(this,0)},get:function(a){return a==null?this.toArray():a<0?this[this.length+a]:this[a]},pushStack:function(a,b,c){var d=this.constructor();e.isArray(a)?E.apply(d,a):e.merge(d,a),d.prevObject=this,d.context=this.context,b==="find"?d.selector=this.selector+(this.selector?" ":"")+c:b&&(d.selector=this.selector+"."+b+"("+c+")");return d},each:function(a,b){return e.each(this,a,b)},ready:function(a){e.bindReady(),A.add(a);return this},eq:function(a){a=+a;return a===-1?this.slice(a):this.slice(a,a+1)},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},slice:function(){return this.pushStack(F.apply(this,arguments),"slice",F.call(arguments).join(","))},map:function(a){return this.pushStack(e.map(this,function(b,c){return a.call(b,c,b)}))},end:function(){return this.prevObject||this.constructor(null)},push:E,sort:[].sort,splice:[].splice},e.fn.init.prototype=e.fn,e.extend=e.fn.extend=function(){var a,c,d,f,g,h,i=arguments[0]||{},j=1,k=arguments.length,l=!1;typeof i=="boolean"&&(l=i,i=arguments[1]||{},j=2),typeof i!="object"&&!e.isFunction(i)&&(i={}),k===j&&(i=this,--j);for(;j0)return;A.fireWith(c,[e]),e.fn.trigger&&e(c).trigger("ready").off("ready")}},bindReady:function(){if(!A){A=e.Callbacks("once memory");if(c.readyState==="complete")return setTimeout(e.ready,1);if(c.addEventListener)c.addEventListener("DOMContentLoaded",B,!1),a.addEventListener("load",e.ready,!1);else if(c.attachEvent){c.attachEvent("onreadystatechange",B),a.attachEvent("onload",e.ready);var b=!1;try{b=a.frameElement==null}catch(d){}c.documentElement.doScroll&&b&&J()}}},isFunction:function(a){return e.type(a)==="function"},isArray:Array.isArray||function(a){return e.type(a)==="array"},isWindow:function(a){return a!=null&&a==a.window},isNumeric:function(a){return!isNaN(parseFloat(a))&&isFinite(a)},type:function(a){return a==null?String(a):I[C.call(a)]||"object"},isPlainObject:function(a){if(!a||e.type(a)!=="object"||a.nodeType||e.isWindow(a))return!1;try{if(a.constructor&&!D.call(a,"constructor")&&!D.call(a.constructor.prototype,"isPrototypeOf"))return!1}catch(c){return!1}var d;for(d in a);return d===b||D.call(a,d)},isEmptyObject:function(a){for(var b in a)return!1;return!0},error:function(a){throw new Error(a)},parseJSON:function(b){if(typeof b!="string"||!b)return null;b=e.trim(b);if(a.JSON&&a.JSON.parse)return a.JSON.parse(b);if(n.test(b.replace(o,"@").replace(p,"]").replace(q,"")))return(new Function("return "+b))();e.error("Invalid JSON: "+b)},parseXML:function(c){if(typeof c!="string"||!c)return null;var d,f;try{a.DOMParser?(f=new DOMParser,d=f.parseFromString(c,"text/xml")):(d=new ActiveXObject("Microsoft.XMLDOM"),d.async="false",d.loadXML(c))}catch(g){d=b}(!d||!d.documentElement||d.getElementsByTagName("parsererror").length)&&e.error("Invalid XML: "+c);return d},noop:function(){},globalEval:function(b){b&&j.test(b)&&(a.execScript||function(b){a.eval.call(a,b)})(b)},camelCase:function(a){return a.replace(w,"ms-").replace(v,x)},nodeName:function(a,b){return a.nodeName&&a.nodeName.toUpperCase()===b.toUpperCase()},each:function(a,c,d){var f,g=0,h=a.length,i=h===b||e.isFunction(a);if(d){if(i){for(f in a)if(c.apply(a[f],d)===!1)break}else for(;g0&&a[0]&&a[j-1]||j===0||e.isArray(a));if(k)for(;i1?i.call(arguments,0):b,j.notifyWith(k,e)}}function l(a){return function(c){b[a]=arguments.length>1?i.call(arguments,0):c,--g||j.resolveWith(j,b)}}var b=i.call(arguments,0),c=0,d=b.length,e=Array(d),g=d,h=d,j=d<=1&&a&&f.isFunction(a.promise)?a:f.Deferred(),k=j.promise();if(d>1){for(;c
a",d=p.getElementsByTagName("*"),e=p.getElementsByTagName("a")[0];if(!d||!d.length||!e)return{};g=c.createElement("select"),h=g.appendChild(c.createElement("option")),i=p.getElementsByTagName("input")[0],b={leadingWhitespace:p.firstChild.nodeType===3,tbody:!p.getElementsByTagName("tbody").length,htmlSerialize:!!p.getElementsByTagName("link").length,style:/top/.test(e.getAttribute("style")),hrefNormalized:e.getAttribute("href")==="/a",opacity:/^0.55/.test(e.style.opacity),cssFloat:!!e.style.cssFloat,checkOn:i.value==="on",optSelected:h.selected,getSetAttribute:p.className!=="t",enctype:!!c.createElement("form").enctype,html5Clone:c.createElement("nav").cloneNode(!0).outerHTML!=="<:nav>",submitBubbles:!0,changeBubbles:!0,focusinBubbles:!1,deleteExpando:!0,noCloneEvent:!0,inlineBlockNeedsLayout:!1,shrinkWrapBlocks:!1,reliableMarginRight:!0,pixelMargin:!0},f.boxModel=b.boxModel=c.compatMode==="CSS1Compat",i.checked=!0,b.noCloneChecked=i.cloneNode(!0).checked,g.disabled=!0,b.optDisabled=!h.disabled;try{delete p.test}catch(r){b.deleteExpando=!1}!p.addEventListener&&p.attachEvent&&p.fireEvent&&(p.attachEvent("onclick",function(){b.noCloneEvent=!1}),p.cloneNode(!0).fireEvent("onclick")),i=c.createElement("input"),i.value="t",i.setAttribute("type","radio"),b.radioValue=i.value==="t",i.setAttribute("checked","checked"),i.setAttribute("name","t"),p.appendChild(i),j=c.createDocumentFragment(),j.appendChild(p.lastChild),b.checkClone=j.cloneNode(!0).cloneNode(!0).lastChild.checked,b.appendChecked=i.checked,j.removeChild(i),j.appendChild(p);if(p.attachEvent)for(n in{submit:1,change:1,focusin:1})m="on"+n,o=m in p,o||(p.setAttribute(m,"return;"),o=typeof p[m]=="function"),b[n+"Bubbles"]=o;j.removeChild(p),j=g=h=p=i=null,f(function(){var d,e,g,h,i,j,l,m,n,q,r,s,t,u=c.getElementsByTagName("body")[0];!u||(m=1,t="padding:0;margin:0;border:",r="position:absolute;top:0;left:0;width:1px;height:1px;",s=t+"0;visibility:hidden;",n="style='"+r+t+"5px solid #000;",q="
"+""+"
",d=c.createElement("div"),d.style.cssText=s+"width:0;height:0;position:static;top:0;margin-top:"+m+"px",u.insertBefore(d,u.firstChild),p=c.createElement("div"),d.appendChild(p),p.innerHTML="
t
",k=p.getElementsByTagName("td"),o=k[0].offsetHeight===0,k[0].style.display="",k[1].style.display="none",b.reliableHiddenOffsets=o&&k[0].offsetHeight===0,a.getComputedStyle&&(p.innerHTML="",l=c.createElement("div"),l.style.width="0",l.style.marginRight="0",p.style.width="2px",p.appendChild(l),b.reliableMarginRight=(parseInt((a.getComputedStyle(l,null)||{marginRight:0}).marginRight,10)||0)===0),typeof p.style.zoom!="undefined"&&(p.innerHTML="",p.style.width=p.style.padding="1px",p.style.border=0,p.style.overflow="hidden",p.style.display="inline",p.style.zoom=1,b.inlineBlockNeedsLayout=p.offsetWidth===3,p.style.display="block",p.style.overflow="visible",p.innerHTML="
",b.shrinkWrapBlocks=p.offsetWidth!==3),p.style.cssText=r+s,p.innerHTML=q,e=p.firstChild,g=e.firstChild,i=e.nextSibling.firstChild.firstChild,j={doesNotAddBorder:g.offsetTop!==5,doesAddBorderForTableAndCells:i.offsetTop===5},g.style.position="fixed",g.style.top="20px",j.fixedPosition=g.offsetTop===20||g.offsetTop===15,g.style.position=g.style.top="",e.style.overflow="hidden",e.style.position="relative",j.subtractsBorderForOverflowNotVisible=g.offsetTop===-5,j.doesNotIncludeMarginInBodyOffset=u.offsetTop!==m,a.getComputedStyle&&(p.style.marginTop="1%",b.pixelMargin=(a.getComputedStyle(p,null)||{marginTop:0}).marginTop!=="1%"),typeof d.style.zoom!="undefined"&&(d.style.zoom=1),u.removeChild(d),l=p=d=null,f.extend(b,j))});return b}();var j=/^(?:\{.*\}|\[.*\])$/,k=/([A-Z])/g;f.extend({cache:{},uuid:0,expando:"jQuery"+(f.fn.jquery+Math.random()).replace(/\D/g,""),noData:{embed:!0,object:"clsid:D27CDB6E-AE6D-11cf-96B8-444553540000",applet:!0},hasData:function(a){a=a.nodeType?f.cache[a[f.expando]]:a[f.expando];return!!a&&!m(a)},data:function(a,c,d,e){if(!!f.acceptData(a)){var g,h,i,j=f.expando,k=typeof c=="string",l=a.nodeType,m=l?f.cache:a,n=l?a[j]:a[j]&&j,o=c==="events";if((!n||!m[n]||!o&&!e&&!m[n].data)&&k&&d===b)return;n||(l?a[j]=n=++f.uuid:n=j),m[n]||(m[n]={},l||(m[n].toJSON=f.noop));if(typeof c=="object"||typeof c=="function")e?m[n]=f.extend(m[n],c):m[n].data=f.extend(m[n].data,c);g=h=m[n],e||(h.data||(h.data={}),h=h.data),d!==b&&(h[f.camelCase(c)]=d);if(o&&!h[c])return g.events;k?(i=h[c],i==null&&(i=h[f.camelCase(c)])):i=h;return i}},removeData:function(a,b,c){if(!!f.acceptData(a)){var d,e,g,h=f.expando,i=a.nodeType,j=i?f.cache:a,k=i?a[h]:h;if(!j[k])return;if(b){d=c?j[k]:j[k].data;if(d){f.isArray(b)||(b in d?b=[b]:(b=f.camelCase(b),b in d?b=[b]:b=b.split(" ")));for(e=0,g=b.length;e1,null,!1)},removeData:function(a){return this.each(function(){f.removeData(this,a)})}}),f.extend({_mark:function(a,b){a&&(b=(b||"fx")+"mark",f._data(a,b,(f._data(a,b)||0)+1))},_unmark:function(a,b,c){a!==!0&&(c=b,b=a,a=!1);if(b){c=c||"fx";var d=c+"mark",e=a?0:(f._data(b,d)||1)-1;e?f._data(b,d,e):(f.removeData(b,d,!0),n(b,c,"mark"))}},queue:function(a,b,c){var d;if(a){b=(b||"fx")+"queue",d=f._data(a,b),c&&(!d||f.isArray(c)?d=f._data(a,b,f.makeArray(c)):d.push(c));return d||[]}},dequeue:function(a,b){b=b||"fx";var c=f.queue(a,b),d=c.shift(),e={};d==="inprogress"&&(d=c.shift()),d&&(b==="fx"&&c.unshift("inprogress"),f._data(a,b+".run",e),d.call(a,function(){f.dequeue(a,b)},e)),c.length||(f.removeData(a,b+"queue "+b+".run",!0),n(a,b,"queue"))}}),f.fn.extend({queue:function(a,c){var d=2;typeof a!="string"&&(c=a,a="fx",d--);if(arguments.length1)},removeAttr:function(a){return this.each(function(){f.removeAttr(this,a)})},prop:function(a,b){return f.access(this,f.prop,a,b,arguments.length>1)},removeProp:function(a){a=f.propFix[a]||a;return this.each(function(){try{this[a]=b,delete this[a]}catch(c){}})},addClass:function(a){var b,c,d,e,g,h,i;if(f.isFunction(a))return this.each(function(b){f(this).addClass(a.call(this,b,this.className))});if(a&&typeof a=="string"){b=a.split(p);for(c=0,d=this.length;c-1)return!0;return!1},val:function(a){var c,d,e,g=this[0];{if(!!arguments.length){e=f.isFunction(a);return this.each(function(d){var g=f(this),h;if(this.nodeType===1){e?h=a.call(this,d,g.val()):h=a,h==null?h="":typeof h=="number"?h+="":f.isArray(h)&&(h=f.map(h,function(a){return a==null?"":a+""})),c=f.valHooks[this.type]||f.valHooks[this.nodeName.toLowerCase()];if(!c||!("set"in c)||c.set(this,h,"value")===b)this.value=h}})}if(g){c=f.valHooks[g.type]||f.valHooks[g.nodeName.toLowerCase()];if(c&&"get"in c&&(d=c.get(g,"value"))!==b)return d;d=g.value;return typeof d=="string"?d.replace(q,""):d==null?"":d}}}}),f.extend({valHooks:{option:{get:function(a){var b=a.attributes.value;return!b||b.specified?a.value:a.text}},select:{get:function(a){var b,c,d,e,g=a.selectedIndex,h=[],i=a.options,j=a.type==="select-one";if(g<0)return null;c=j?g:0,d=j?g+1:i.length;for(;c=0}),c.length||(a.selectedIndex=-1);return c}}},attrFn:{val:!0,css:!0,html:!0,text:!0,data:!0,width:!0,height:!0,offset:!0},attr:function(a,c,d,e){var g,h,i,j=a.nodeType;if(!!a&&j!==3&&j!==8&&j!==2){if(e&&c in f.attrFn)return f(a)[c](d);if(typeof a.getAttribute=="undefined")return f.prop(a,c,d);i=j!==1||!f.isXMLDoc(a),i&&(c=c.toLowerCase(),h=f.attrHooks[c]||(u.test(c)?x:w));if(d!==b){if(d===null){f.removeAttr(a,c);return}if(h&&"set"in h&&i&&(g=h.set(a,d,c))!==b)return g;a.setAttribute(c,""+d);return d}if(h&&"get"in h&&i&&(g=h.get(a,c))!==null)return g;g=a.getAttribute(c);return g===null?b:g}},removeAttr:function(a,b){var c,d,e,g,h,i=0;if(b&&a.nodeType===1){d=b.toLowerCase().split(p),g=d.length;for(;i=0}})});var z=/^(?:textarea|input|select)$/i,A=/^([^\.]*)?(?:\.(.+))?$/,B=/(?:^|\s)hover(\.\S+)?\b/,C=/^key/,D=/^(?:mouse|contextmenu)|click/,E=/^(?:focusinfocus|focusoutblur)$/,F=/^(\w*)(?:#([\w\-]+))?(?:\.([\w\-]+))?$/,G=function( +a){var b=F.exec(a);b&&(b[1]=(b[1]||"").toLowerCase(),b[3]=b[3]&&new RegExp("(?:^|\\s)"+b[3]+"(?:\\s|$)"));return b},H=function(a,b){var c=a.attributes||{};return(!b[1]||a.nodeName.toLowerCase()===b[1])&&(!b[2]||(c.id||{}).value===b[2])&&(!b[3]||b[3].test((c["class"]||{}).value))},I=function(a){return f.event.special.hover?a:a.replace(B,"mouseenter$1 mouseleave$1")};f.event={add:function(a,c,d,e,g){var h,i,j,k,l,m,n,o,p,q,r,s;if(!(a.nodeType===3||a.nodeType===8||!c||!d||!(h=f._data(a)))){d.handler&&(p=d,d=p.handler,g=p.selector),d.guid||(d.guid=f.guid++),j=h.events,j||(h.events=j={}),i=h.handle,i||(h.handle=i=function(a){return typeof f!="undefined"&&(!a||f.event.triggered!==a.type)?f.event.dispatch.apply(i.elem,arguments):b},i.elem=a),c=f.trim(I(c)).split(" ");for(k=0;k=0&&(h=h.slice(0,-1),k=!0),h.indexOf(".")>=0&&(i=h.split("."),h=i.shift(),i.sort());if((!e||f.event.customEvent[h])&&!f.event.global[h])return;c=typeof c=="object"?c[f.expando]?c:new f.Event(h,c):new f.Event(h),c.type=h,c.isTrigger=!0,c.exclusive=k,c.namespace=i.join("."),c.namespace_re=c.namespace?new RegExp("(^|\\.)"+i.join("\\.(?:.*\\.)?")+"(\\.|$)"):null,o=h.indexOf(":")<0?"on"+h:"";if(!e){j=f.cache;for(l in j)j[l].events&&j[l].events[h]&&f.event.trigger(c,d,j[l].handle.elem,!0);return}c.result=b,c.target||(c.target=e),d=d!=null?f.makeArray(d):[],d.unshift(c),p=f.event.special[h]||{};if(p.trigger&&p.trigger.apply(e,d)===!1)return;r=[[e,p.bindType||h]];if(!g&&!p.noBubble&&!f.isWindow(e)){s=p.delegateType||h,m=E.test(s+h)?e:e.parentNode,n=null;for(;m;m=m.parentNode)r.push([m,s]),n=m;n&&n===e.ownerDocument&&r.push([n.defaultView||n.parentWindow||a,s])}for(l=0;le&&j.push({elem:this,matches:d.slice(e)});for(k=0;k0?this.on(b,null,a,c):this.trigger(b)},f.attrFn&&(f.attrFn[b]=!0),C.test(b)&&(f.event.fixHooks[b]=f.event.keyHooks),D.test(b)&&(f.event.fixHooks[b]=f.event.mouseHooks)}),function(){function x(a,b,c,e,f,g){for(var h=0,i=e.length;h0){k=j;break}}j=j[a]}e[h]=k}}}function w(a,b,c,e,f,g){for(var h=0,i=e.length;h+~,(\[\\]+)+|[>+~])(\s*,\s*)?((?:.|\r|\n)*)/g,d="sizcache"+(Math.random()+"").replace(".",""),e=0,g=Object.prototype.toString,h=!1,i=!0,j=/\\/g,k=/\r\n/g,l=/\W/;[0,0].sort(function(){i=!1;return 0});var m=function(b,d,e,f){e=e||[],d=d||c;var h=d;if(d.nodeType!==1&&d.nodeType!==9)return[];if(!b||typeof b!="string")return e;var i,j,k,l,n,q,r,t,u=!0,v=m.isXML(d),w=[],x=b;do{a.exec(""),i=a.exec(x);if(i){x=i[3],w.push(i[1]);if(i[2]){l=i[3];break}}}while(i);if(w.length>1&&p.exec(b))if(w.length===2&&o.relative[w[0]])j=y(w[0]+w[1],d,f);else{j=o.relative[w[0]]?[d]:m(w.shift(),d);while(w.length)b=w.shift(),o.relative[b]&&(b+=w.shift()),j=y(b,j,f)}else{!f&&w.length>1&&d.nodeType===9&&!v&&o.match.ID.test(w[0])&&!o.match.ID.test(w[w.length-1])&&(n=m.find(w.shift(),d,v),d=n.expr?m.filter(n.expr,n.set)[0]:n.set[0]);if(d){n=f?{expr:w.pop(),set:s(f)}:m.find(w.pop(),w.length===1&&(w[0]==="~"||w[0]==="+")&&d.parentNode?d.parentNode:d,v),j=n.expr?m.filter(n.expr,n.set):n.set,w.length>0?k=s(j):u=!1;while(w.length)q=w.pop(),r=q,o.relative[q]?r=w.pop():q="",r==null&&(r=d),o.relative[q](k,r,v)}else k=w=[]}k||(k=j),k||m.error(q||b);if(g.call(k)==="[object Array]")if(!u)e.push.apply(e,k);else if(d&&d.nodeType===1)for(t=0;k[t]!=null;t++)k[t]&&(k[t]===!0||k[t].nodeType===1&&m.contains(d,k[t]))&&e.push(j[t]);else for(t=0;k[t]!=null;t++)k[t]&&k[t].nodeType===1&&e.push(j[t]);else s(k,e);l&&(m(l,h,e,f),m.uniqueSort(e));return e};m.uniqueSort=function(a){if(u){h=i,a.sort(u);if(h)for(var b=1;b0},m.find=function(a,b,c){var d,e,f,g,h,i;if(!a)return[];for(e=0,f=o.order.length;e":function(a,b){var c,d=typeof b=="string",e=0,f=a.length;if(d&&!l.test(b)){b=b.toLowerCase();for(;e=0)?c||d.push(h):c&&(b[g]=!1));return!1},ID:function(a){return a[1].replace(j,"")},TAG:function(a,b){return a[1].replace(j,"").toLowerCase()},CHILD:function(a){if(a[1]==="nth"){a[2]||m.error(a[0]),a[2]=a[2].replace(/^\+|\s*/g,"");var b=/(-?)(\d*)(?:n([+\-]?\d*))?/.exec(a[2]==="even"&&"2n"||a[2]==="odd"&&"2n+1"||!/\D/.test(a[2])&&"0n+"+a[2]||a[2]);a[2]=b[1]+(b[2]||1)-0,a[3]=b[3]-0}else a[2]&&m.error(a[0]);a[0]=e++;return a},ATTR:function(a,b,c,d,e,f){var g=a[1]=a[1].replace(j,"");!f&&o.attrMap[g]&&(a[1]=o.attrMap[g]),a[4]=(a[4]||a[5]||"").replace(j,""),a[2]==="~="&&(a[4]=" "+a[4]+" ");return a},PSEUDO:function(b,c,d,e,f){if(b[1]==="not")if((a.exec(b[3])||"").length>1||/^\w/.test(b[3]))b[3]=m(b[3],null,null,c);else{var g=m.filter(b[3],c,d,!0^f);d||e.push.apply(e,g);return!1}else if(o.match.POS.test(b[0])||o.match.CHILD.test(b[0]))return!0;return b},POS:function(a){a.unshift(!0);return a}},filters:{enabled:function(a){return a.disabled===!1&&a.type!=="hidden"},disabled:function(a){return a.disabled===!0},checked:function(a){return a.checked===!0},selected:function(a){a.parentNode&&a.parentNode.selectedIndex;return a.selected===!0},parent:function(a){return!!a.firstChild},empty:function(a){return!a.firstChild},has:function(a,b,c){return!!m(c[3],a).length},header:function(a){return/h\d/i.test(a.nodeName)},text:function(a){var b=a.getAttribute("type"),c=a.type;return a.nodeName.toLowerCase()==="input"&&"text"===c&&(b===c||b===null)},radio:function(a){return a.nodeName.toLowerCase()==="input"&&"radio"===a.type},checkbox:function(a){return a.nodeName.toLowerCase()==="input"&&"checkbox"===a.type},file:function(a){return a.nodeName.toLowerCase()==="input"&&"file"===a.type},password:function(a){return a.nodeName.toLowerCase()==="input"&&"password"===a.type},submit:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"submit"===a.type},image:function(a){return a.nodeName.toLowerCase()==="input"&&"image"===a.type},reset:function(a){var b=a.nodeName.toLowerCase();return(b==="input"||b==="button")&&"reset"===a.type},button:function(a){var b=a.nodeName.toLowerCase();return b==="input"&&"button"===a.type||b==="button"},input:function(a){return/input|select|textarea|button/i.test(a.nodeName)},focus:function(a){return a===a.ownerDocument.activeElement}},setFilters:{first:function(a,b){return b===0},last:function(a,b,c,d){return b===d.length-1},even:function(a,b){return b%2===0},odd:function(a,b){return b%2===1},lt:function(a,b,c){return bc[3]-0},nth:function(a,b,c){return c[3]-0===b},eq:function(a,b,c){return c[3]-0===b}},filter:{PSEUDO:function(a,b,c,d){var e=b[1],f=o.filters[e];if(f)return f(a,c,b,d);if(e==="contains")return(a.textContent||a.innerText||n([a])||"").indexOf(b[3])>=0;if(e==="not"){var g=b[3];for(var h=0,i=g.length;h=0}},ID:function(a,b){return a.nodeType===1&&a.getAttribute("id")===b},TAG:function(a,b){return b==="*"&&a.nodeType===1||!!a.nodeName&&a.nodeName.toLowerCase()===b},CLASS:function(a,b){return(" "+(a.className||a.getAttribute("class"))+" ").indexOf(b)>-1},ATTR:function(a,b){var c=b[1],d=m.attr?m.attr(a,c):o.attrHandle[c]?o.attrHandle[c](a):a[c]!=null?a[c]:a.getAttribute(c),e=d+"",f=b[2],g=b[4];return d==null?f==="!=":!f&&m.attr?d!=null:f==="="?e===g:f==="*="?e.indexOf(g)>=0:f==="~="?(" "+e+" ").indexOf(g)>=0:g?f==="!="?e!==g:f==="^="?e.indexOf(g)===0:f==="$="?e.substr(e.length-g.length)===g:f==="|="?e===g||e.substr(0,g.length+1)===g+"-":!1:e&&d!==!1},POS:function(a,b,c,d){var e=b[2],f=o.setFilters[e];if(f)return f(a,c,b,d)}}},p=o.match.POS,q=function(a,b){return"\\"+(b-0+1)};for(var r in o.match)o.match[r]=new RegExp(o.match[r].source+/(?![^\[]*\])(?![^\(]*\))/.source),o.leftMatch[r]=new RegExp(/(^(?:.|\r|\n)*?)/.source+o.match[r].source.replace(/\\(\d+)/g,q));o.match.globalPOS=p;var s=function(a,b){a=Array.prototype.slice.call(a,0);if(b){b.push.apply(b,a);return b}return a};try{Array.prototype.slice.call(c.documentElement.childNodes,0)[0].nodeType}catch(t){s=function(a,b){var c=0,d=b||[];if(g.call(a)==="[object Array]")Array.prototype.push.apply(d,a);else if(typeof a.length=="number")for(var e=a.length;c",e.insertBefore(a,e.firstChild),c.getElementById(d)&&(o.find.ID=function(a,c,d){if(typeof c.getElementById!="undefined"&&!d){var e=c.getElementById(a[1]);return e?e.id===a[1]||typeof e.getAttributeNode!="undefined"&&e.getAttributeNode("id").nodeValue===a[1]?[e]:b:[]}},o.filter.ID=function(a,b){var c=typeof a.getAttributeNode!="undefined"&&a.getAttributeNode("id");return a.nodeType===1&&c&&c.nodeValue===b}),e.removeChild(a),e=a=null}(),function(){var a=c.createElement("div");a.appendChild(c.createComment("")),a.getElementsByTagName("*").length>0&&(o.find.TAG=function(a,b){var c=b.getElementsByTagName(a[1]);if(a[1]==="*"){var d=[];for(var e=0;c[e];e++)c[e].nodeType===1&&d.push(c[e]);c=d}return c}),a.innerHTML="",a.firstChild&&typeof a.firstChild.getAttribute!="undefined"&&a.firstChild.getAttribute("href")!=="#"&&(o.attrHandle.href=function(a){return a.getAttribute("href",2)}),a=null}(),c.querySelectorAll&&function(){var a=m,b=c.createElement("div"),d="__sizzle__";b.innerHTML="

";if(!b.querySelectorAll||b.querySelectorAll(".TEST").length!==0){m=function(b,e,f,g){e=e||c;if(!g&&!m.isXML(e)){var h=/^(\w+$)|^\.([\w\-]+$)|^#([\w\-]+$)/.exec(b);if(h&&(e.nodeType===1||e.nodeType===9)){if(h[1])return s(e.getElementsByTagName(b),f);if(h[2]&&o.find.CLASS&&e.getElementsByClassName)return s(e.getElementsByClassName(h[2]),f)}if(e.nodeType===9){if(b==="body"&&e.body)return s([e.body],f);if(h&&h[3]){var i=e.getElementById(h[3]);if(!i||!i.parentNode)return s([],f);if(i.id===h[3])return s([i],f)}try{return s(e.querySelectorAll(b),f)}catch(j){}}else if(e.nodeType===1&&e.nodeName.toLowerCase()!=="object"){var k=e,l=e.getAttribute("id"),n=l||d,p=e.parentNode,q=/^\s*[+~]/.test(b);l?n=n.replace(/'/g,"\\$&"):e.setAttribute("id",n),q&&p&&(e=e.parentNode);try{if(!q||p)return s(e.querySelectorAll("[id='"+n+"'] "+b),f)}catch(r){}finally{l||k.removeAttribute("id")}}}return a(b,e,f,g)};for(var e in a)m[e]=a[e];b=null}}(),function(){var a=c.documentElement,b=a.matchesSelector||a.mozMatchesSelector||a.webkitMatchesSelector||a.msMatchesSelector;if(b){var d=!b.call(c.createElement("div"),"div"),e=!1;try{b.call(c.documentElement,"[test!='']:sizzle")}catch(f){e=!0}m.matchesSelector=function(a,c){c=c.replace(/\=\s*([^'"\]]*)\s*\]/g,"='$1']");if(!m.isXML(a))try{if(e||!o.match.PSEUDO.test(c)&&!/!=/.test(c)){var f=b.call(a,c);if(f||!d||a.document&&a.document.nodeType!==11)return f}}catch(g){}return m(c,null,null,[a]).length>0}}}(),function(){var a=c.createElement("div");a.innerHTML="
";if(!!a.getElementsByClassName&&a.getElementsByClassName("e").length!==0){a.lastChild.className="e";if(a.getElementsByClassName("e").length===1)return;o.order.splice(1,0,"CLASS"),o.find.CLASS=function(a,b,c){if(typeof b.getElementsByClassName!="undefined"&&!c)return b.getElementsByClassName(a[1])},a=null}}(),c.documentElement.contains?m.contains=function(a,b){return a!==b&&(a.contains?a.contains(b):!0)}:c.documentElement.compareDocumentPosition?m.contains=function(a,b){return!!(a.compareDocumentPosition(b)&16)}:m.contains=function(){return!1},m.isXML=function(a){var b=(a?a.ownerDocument||a:0).documentElement;return b?b.nodeName!=="HTML":!1};var y=function(a,b,c){var d,e=[],f="",g=b.nodeType?[b]:b;while(d=o.match.PSEUDO.exec(a))f+=d[0],a=a.replace(o.match.PSEUDO,"");a=o.relative[a]?a+"*":a;for(var h=0,i=g.length;h0)for(h=g;h=0:f.filter(a,this).length>0:this.filter(a).length>0)},closest:function(a,b){var c=[],d,e,g=this[0];if(f.isArray(a)){var h=1;while(g&&g.ownerDocument&&g!==b){for(d=0;d-1:f.find.matchesSelector(g,a)){c.push(g);break}g=g.parentNode;if(!g||!g.ownerDocument||g===b||g.nodeType===11)break}}c=c.length>1?f.unique(c):c;return this.pushStack(c,"closest",a)},index:function(a){if(!a)return this[0]&&this[0].parentNode?this.prevAll().length:-1;if(typeof a=="string")return f.inArray(this[0],f(a));return f.inArray(a.jquery?a[0]:a,this)},add:function(a,b){var c=typeof a=="string"?f(a,b):f.makeArray(a&&a.nodeType?[a]:a),d=f.merge(this.get(),c);return this.pushStack(S(c[0])||S(d[0])?d:f.unique(d))},andSelf:function(){return this.add(this.prevObject)}}),f.each({parent:function(a){var b=a.parentNode;return b&&b.nodeType!==11?b:null},parents:function(a){return f.dir(a,"parentNode")},parentsUntil:function(a,b,c){return f.dir(a,"parentNode",c)},next:function(a){return f.nth(a,2,"nextSibling")},prev:function(a){return f.nth(a,2,"previousSibling")},nextAll:function(a){return f.dir(a,"nextSibling")},prevAll:function(a){return f.dir(a,"previousSibling")},nextUntil:function(a,b,c){return f.dir(a,"nextSibling",c)},prevUntil:function(a,b,c){return f.dir(a,"previousSibling",c)},siblings:function(a){return f.sibling((a.parentNode||{}).firstChild,a)},children:function(a){return f.sibling(a.firstChild)},contents:function(a){return f.nodeName(a,"iframe")?a.contentDocument||a.contentWindow.document:f.makeArray(a.childNodes)}},function(a,b){f.fn[a]=function(c,d){var e=f.map(this,b,c);L.test(a)||(d=c),d&&typeof d=="string"&&(e=f.filter(d,e)),e=this.length>1&&!R[a]?f.unique(e):e,(this.length>1||N.test(d))&&M.test(a)&&(e=e.reverse());return this.pushStack(e,a,P.call(arguments).join(","))}}),f.extend({filter:function(a,b,c){c&&(a=":not("+a+")");return b.length===1?f.find.matchesSelector(b[0],a)?[b[0]]:[]:f.find.matches(a,b)},dir:function(a,c,d){var e=[],g=a[c];while(g&&g.nodeType!==9&&(d===b||g.nodeType!==1||!f(g).is(d)))g.nodeType===1&&e.push(g),g=g[c];return e},nth:function(a,b,c,d){b=b||1;var e=0;for(;a;a=a[c])if(a.nodeType===1&&++e===b)break;return a},sibling:function(a,b){var c=[];for(;a;a=a.nextSibling)a.nodeType===1&&a!==b&&c.push(a);return c}});var V="abbr|article|aside|audio|bdi|canvas|data|datalist|details|figcaption|figure|footer|header|hgroup|mark|meter|nav|output|progress|section|summary|time|video",W=/ jQuery\d+="(?:\d+|null)"/g,X=/^\s+/,Y=/<(?!area|br|col|embed|hr|img|input|link|meta|param)(([\w:]+)[^>]*)\/>/ig,Z=/<([\w:]+)/,$=/]","i"),bd=/checked\s*(?:[^=]|=\s*.checked.)/i,be=/\/(java|ecma)script/i,bf=/^\s*",""],legend:[1,"
","
"],thead:[1,"","
"],tr:[2,"","
"],td:[3,"","
"],col:[2,"","
"],area:[1,"",""],_default:[0,"",""]},bh=U(c);bg.optgroup=bg.option,bg.tbody=bg.tfoot=bg.colgroup=bg.caption=bg.thead,bg.th=bg.td,f.support.htmlSerialize||(bg._default=[1,"div
","
"]),f.fn.extend({text:function(a){return f.access(this,function(a){return a===b?f.text(this):this.empty().append((this[0]&&this[0].ownerDocument||c).createTextNode(a))},null,a,arguments.length)},wrapAll:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapAll(a.call(this,b))});if(this[0]){var b=f(a,this[0].ownerDocument).eq(0).clone(!0);this[0].parentNode&&b.insertBefore(this[0]),b.map(function(){var a=this;while(a.firstChild&&a.firstChild.nodeType===1)a=a.firstChild;return a}).append(this)}return this},wrapInner:function(a){if(f.isFunction(a))return this.each(function(b){f(this).wrapInner(a.call(this,b))});return this.each(function(){var b=f(this),c=b.contents();c.length?c.wrapAll(a):b.append(a)})},wrap:function(a){var b=f.isFunction(a);return this.each(function(c){f(this).wrapAll(b?a.call(this,c):a)})},unwrap:function(){return this.parent().each(function(){f.nodeName(this,"body")||f(this).replaceWith(this.childNodes)}).end()},append:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.appendChild(a)})},prepend:function(){return this.domManip(arguments,!0,function(a){this.nodeType===1&&this.insertBefore(a,this.firstChild)})},before:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this)});if(arguments.length){var a=f +.clean(arguments);a.push.apply(a,this.toArray());return this.pushStack(a,"before",arguments)}},after:function(){if(this[0]&&this[0].parentNode)return this.domManip(arguments,!1,function(a){this.parentNode.insertBefore(a,this.nextSibling)});if(arguments.length){var a=this.pushStack(this,"after",arguments);a.push.apply(a,f.clean(arguments));return a}},remove:function(a,b){for(var c=0,d;(d=this[c])!=null;c++)if(!a||f.filter(a,[d]).length)!b&&d.nodeType===1&&(f.cleanData(d.getElementsByTagName("*")),f.cleanData([d])),d.parentNode&&d.parentNode.removeChild(d);return this},empty:function(){for(var a=0,b;(b=this[a])!=null;a++){b.nodeType===1&&f.cleanData(b.getElementsByTagName("*"));while(b.firstChild)b.removeChild(b.firstChild)}return this},clone:function(a,b){a=a==null?!1:a,b=b==null?a:b;return this.map(function(){return f.clone(this,a,b)})},html:function(a){return f.access(this,function(a){var c=this[0]||{},d=0,e=this.length;if(a===b)return c.nodeType===1?c.innerHTML.replace(W,""):null;if(typeof a=="string"&&!ba.test(a)&&(f.support.leadingWhitespace||!X.test(a))&&!bg[(Z.exec(a)||["",""])[1].toLowerCase()]){a=a.replace(Y,"<$1>");try{for(;d1&&l0?this.clone(!0):this).get();f(e[h])[b](j),d=d.concat(j)}return this.pushStack(d,a,e.selector)}}),f.extend({clone:function(a,b,c){var d,e,g,h=f.support.html5Clone||f.isXMLDoc(a)||!bc.test("<"+a.nodeName+">")?a.cloneNode(!0):bo(a);if((!f.support.noCloneEvent||!f.support.noCloneChecked)&&(a.nodeType===1||a.nodeType===11)&&!f.isXMLDoc(a)){bk(a,h),d=bl(a),e=bl(h);for(g=0;d[g];++g)e[g]&&bk(d[g],e[g])}if(b){bj(a,h);if(c){d=bl(a),e=bl(h);for(g=0;d[g];++g)bj(d[g],e[g])}}d=e=null;return h},clean:function(a,b,d,e){var g,h,i,j=[];b=b||c,typeof b.createElement=="undefined"&&(b=b.ownerDocument||b[0]&&b[0].ownerDocument||c);for(var k=0,l;(l=a[k])!=null;k++){typeof l=="number"&&(l+="");if(!l)continue;if(typeof l=="string")if(!_.test(l))l=b.createTextNode(l);else{l=l.replace(Y,"<$1>");var m=(Z.exec(l)||["",""])[1].toLowerCase(),n=bg[m]||bg._default,o=n[0],p=b.createElement("div"),q=bh.childNodes,r;b===c?bh.appendChild(p):U(b).appendChild(p),p.innerHTML=n[1]+l+n[2];while(o--)p=p.lastChild;if(!f.support.tbody){var s=$.test(l),t=m==="table"&&!s?p.firstChild&&p.firstChild.childNodes:n[1]===""&&!s?p.childNodes:[];for(i=t.length-1;i>=0;--i)f.nodeName(t[i],"tbody")&&!t[i].childNodes.length&&t[i].parentNode.removeChild(t[i])}!f.support.leadingWhitespace&&X.test(l)&&p.insertBefore(b.createTextNode(X.exec(l)[0]),p.firstChild),l=p.childNodes,p&&(p.parentNode.removeChild(p),q.length>0&&(r=q[q.length-1],r&&r.parentNode&&r.parentNode.removeChild(r)))}var u;if(!f.support.appendChecked)if(l[0]&&typeof (u=l.length)=="number")for(i=0;i1)},f.extend({cssHooks:{opacity:{get:function(a,b){if(b){var c=by(a,"opacity");return c===""?"1":c}return a.style.opacity}}},cssNumber:{fillOpacity:!0,fontWeight:!0,lineHeight:!0,opacity:!0,orphans:!0,widows:!0,zIndex:!0,zoom:!0},cssProps:{"float":f.support.cssFloat?"cssFloat":"styleFloat"},style:function(a,c,d,e){if(!!a&&a.nodeType!==3&&a.nodeType!==8&&!!a.style){var g,h,i=f.camelCase(c),j=a.style,k=f.cssHooks[i];c=f.cssProps[i]||i;if(d===b){if(k&&"get"in k&&(g=k.get(a,!1,e))!==b)return g;return j[c]}h=typeof d,h==="string"&&(g=bu.exec(d))&&(d=+(g[1]+1)*+g[2]+parseFloat(f.css(a,c)),h="number");if(d==null||h==="number"&&isNaN(d))return;h==="number"&&!f.cssNumber[i]&&(d+="px");if(!k||!("set"in k)||(d=k.set(a,d))!==b)try{j[c]=d}catch(l){}}},css:function(a,c,d){var e,g;c=f.camelCase(c),g=f.cssHooks[c],c=f.cssProps[c]||c,c==="cssFloat"&&(c="float");if(g&&"get"in g&&(e=g.get(a,!0,d))!==b)return e;if(by)return by(a,c)},swap:function(a,b,c){var d={},e,f;for(f in b)d[f]=a.style[f],a.style[f]=b[f];e=c.call(a);for(f in b)a.style[f]=d[f];return e}}),f.curCSS=f.css,c.defaultView&&c.defaultView.getComputedStyle&&(bz=function(a,b){var c,d,e,g,h=a.style;b=b.replace(br,"-$1").toLowerCase(),(d=a.ownerDocument.defaultView)&&(e=d.getComputedStyle(a,null))&&(c=e.getPropertyValue(b),c===""&&!f.contains(a.ownerDocument.documentElement,a)&&(c=f.style(a,b))),!f.support.pixelMargin&&e&&bv.test(b)&&bt.test(c)&&(g=h.width,h.width=c,c=e.width,h.width=g);return c}),c.documentElement.currentStyle&&(bA=function(a,b){var c,d,e,f=a.currentStyle&&a.currentStyle[b],g=a.style;f==null&&g&&(e=g[b])&&(f=e),bt.test(f)&&(c=g.left,d=a.runtimeStyle&&a.runtimeStyle.left,d&&(a.runtimeStyle.left=a.currentStyle.left),g.left=b==="fontSize"?"1em":f,f=g.pixelLeft+"px",g.left=c,d&&(a.runtimeStyle.left=d));return f===""?"auto":f}),by=bz||bA,f.each(["height","width"],function(a,b){f.cssHooks[b]={get:function(a,c,d){if(c)return a.offsetWidth!==0?bB(a,b,d):f.swap(a,bw,function(){return bB(a,b,d)})},set:function(a,b){return bs.test(b)?b+"px":b}}}),f.support.opacity||(f.cssHooks.opacity={get:function(a,b){return bq.test((b&&a.currentStyle?a.currentStyle.filter:a.style.filter)||"")?parseFloat(RegExp.$1)/100+"":b?"1":""},set:function(a,b){var c=a.style,d=a.currentStyle,e=f.isNumeric(b)?"alpha(opacity="+b*100+")":"",g=d&&d.filter||c.filter||"";c.zoom=1;if(b>=1&&f.trim(g.replace(bp,""))===""){c.removeAttribute("filter");if(d&&!d.filter)return}c.filter=bp.test(g)?g.replace(bp,e):g+" "+e}}),f(function(){f.support.reliableMarginRight||(f.cssHooks.marginRight={get:function(a,b){return f.swap(a,{display:"inline-block"},function(){return b?by(a,"margin-right"):a.style.marginRight})}})}),f.expr&&f.expr.filters&&(f.expr.filters.hidden=function(a){var b=a.offsetWidth,c=a.offsetHeight;return b===0&&c===0||!f.support.reliableHiddenOffsets&&(a.style&&a.style.display||f.css(a,"display"))==="none"},f.expr.filters.visible=function(a){return!f.expr.filters.hidden(a)}),f.each({margin:"",padding:"",border:"Width"},function(a,b){f.cssHooks[a+b]={expand:function(c){var d,e=typeof c=="string"?c.split(" "):[c],f={};for(d=0;d<4;d++)f[a+bx[d]+b]=e[d]||e[d-2]||e[0];return f}}});var bC=/%20/g,bD=/\[\]$/,bE=/\r?\n/g,bF=/#.*$/,bG=/^(.*?):[ \t]*([^\r\n]*)\r?$/mg,bH=/^(?:color|date|datetime|datetime-local|email|hidden|month|number|password|range|search|tel|text|time|url|week)$/i,bI=/^(?:about|app|app\-storage|.+\-extension|file|res|widget):$/,bJ=/^(?:GET|HEAD)$/,bK=/^\/\//,bL=/\?/,bM=/)<[^<]*)*<\/script>/gi,bN=/^(?:select|textarea)/i,bO=/\s+/,bP=/([?&])_=[^&]*/,bQ=/^([\w\+\.\-]+:)(?:\/\/([^\/?#:]*)(?::(\d+))?)?/,bR=f.fn.load,bS={},bT={},bU,bV,bW=["*/"]+["*"];try{bU=e.href}catch(bX){bU=c.createElement("a"),bU.href="",bU=bU.href}bV=bQ.exec(bU.toLowerCase())||[],f.fn.extend({load:function(a,c,d){if(typeof a!="string"&&bR)return bR.apply(this,arguments);if(!this.length)return this;var e=a.indexOf(" ");if(e>=0){var g=a.slice(e,a.length);a=a.slice(0,e)}var h="GET";c&&(f.isFunction(c)?(d=c,c=b):typeof c=="object"&&(c=f.param(c,f.ajaxSettings.traditional),h="POST"));var i=this;f.ajax({url:a,type:h,dataType:"html",data:c,complete:function(a,b,c){c=a.responseText,a.isResolved()&&(a.done(function(a){c=a}),i.html(g?f("
").append(c.replace(bM,"")).find(g):c)),d&&i.each(d,[c,b,a])}});return this},serialize:function(){return f.param(this.serializeArray())},serializeArray:function(){return this.map(function(){return this.elements?f.makeArray(this.elements):this}).filter(function(){return this.name&&!this.disabled&&(this.checked||bN.test(this.nodeName)||bH.test(this.type))}).map(function(a,b){var c=f(this).val();return c==null?null:f.isArray(c)?f.map(c,function(a,c){return{name:b.name,value:a.replace(bE,"\r\n")}}):{name:b.name,value:c.replace(bE,"\r\n")}}).get()}}),f.each("ajaxStart ajaxStop ajaxComplete ajaxError ajaxSuccess ajaxSend".split(" "),function(a,b){f.fn[b]=function(a){return this.on(b,a)}}),f.each(["get","post"],function(a,c){f[c]=function(a,d,e,g){f.isFunction(d)&&(g=g||e,e=d,d=b);return f.ajax({type:c,url:a,data:d,success:e,dataType:g})}}),f.extend({getScript:function(a,c){return f.get(a,b,c,"script")},getJSON:function(a,b,c){return f.get(a,b,c,"json")},ajaxSetup:function(a,b){b?b$(a,f.ajaxSettings):(b=a,a=f.ajaxSettings),b$(a,b);return a},ajaxSettings:{url:bU,isLocal:bI.test(bV[1]),global:!0,type:"GET",contentType:"application/x-www-form-urlencoded; charset=UTF-8",processData:!0,async:!0,accepts:{xml:"application/xml, text/xml",html:"text/html",text:"text/plain",json:"application/json, text/javascript","*":bW},contents:{xml:/xml/,html:/html/,json:/json/},responseFields:{xml:"responseXML",text:"responseText"},converters:{"* text":a.String,"text html":!0,"text json":f.parseJSON,"text xml":f.parseXML},flatOptions:{context:!0,url:!0}},ajaxPrefilter:bY(bS),ajaxTransport:bY(bT),ajax:function(a,c){function w(a,c,l,m){if(s!==2){s=2,q&&clearTimeout(q),p=b,n=m||"",v.readyState=a>0?4:0;var o,r,u,w=c,x=l?ca(d,v,l):b,y,z;if(a>=200&&a<300||a===304){if(d.ifModified){if(y=v.getResponseHeader("Last-Modified"))f.lastModified[k]=y;if(z=v.getResponseHeader("Etag"))f.etag[k]=z}if(a===304)w="notmodified",o=!0;else try{r=cb(d,x),w="success",o=!0}catch(A){w="parsererror",u=A}}else{u=w;if(!w||a)w="error",a<0&&(a=0)}v.status=a,v.statusText=""+(c||w),o?h.resolveWith(e,[r,w,v]):h.rejectWith(e,[v,w,u]),v.statusCode(j),j=b,t&&g.trigger("ajax"+(o?"Success":"Error"),[v,d,o?r:u]),i.fireWith(e,[v,w]),t&&(g.trigger("ajaxComplete",[v,d]),--f.active||f.event.trigger("ajaxStop"))}}typeof a=="object"&&(c=a,a=b),c=c||{};var d=f.ajaxSetup({},c),e=d.context||d,g=e!==d&&(e.nodeType||e instanceof f)?f(e):f.event,h=f.Deferred(),i=f.Callbacks("once memory"),j=d.statusCode||{},k,l={},m={},n,o,p,q,r,s=0,t,u,v={readyState:0,setRequestHeader:function(a,b){if(!s){var c=a.toLowerCase();a=m[c]=m[c]||a,l[a]=b}return this},getAllResponseHeaders:function(){return s===2?n:null},getResponseHeader:function(a){var c;if(s===2){if(!o){o={};while(c=bG.exec(n))o[c[1].toLowerCase()]=c[2]}c=o[a.toLowerCase()]}return c===b?null:c},overrideMimeType:function(a){s||(d.mimeType=a);return this},abort:function(a){a=a||"abort",p&&p.abort(a),w(0,a);return this}};h.promise(v),v.success=v.done,v.error=v.fail,v.complete=i.add,v.statusCode=function(a){if(a){var b;if(s<2)for(b in a)j[b]=[j[b],a[b]];else b=a[v.status],v.then(b,b)}return this},d.url=((a||d.url)+"").replace(bF,"").replace(bK,bV[1]+"//"),d.dataTypes=f.trim(d.dataType||"*").toLowerCase().split(bO),d.crossDomain==null&&(r=bQ.exec(d.url.toLowerCase()),d.crossDomain=!(!r||r[1]==bV[1]&&r[2]==bV[2]&&(r[3]||(r[1]==="http:"?80:443))==(bV[3]||(bV[1]==="http:"?80:443)))),d.data&&d.processData&&typeof d.data!="string"&&(d.data=f.param(d.data,d.traditional)),bZ(bS,d,c,v);if(s===2)return!1;t=d.global,d.type=d.type.toUpperCase(),d.hasContent=!bJ.test(d.type),t&&f.active++===0&&f.event.trigger("ajaxStart");if(!d.hasContent){d.data&&(d.url+=(bL.test(d.url)?"&":"?")+d.data,delete d.data),k=d.url;if(d.cache===!1){var x=f.now(),y=d.url.replace(bP,"$1_="+x);d.url=y+(y===d.url?(bL.test(d.url)?"&":"?")+"_="+x:"")}}(d.data&&d.hasContent&&d.contentType!==!1||c.contentType)&&v.setRequestHeader("Content-Type",d.contentType),d.ifModified&&(k=k||d.url,f.lastModified[k]&&v.setRequestHeader("If-Modified-Since",f.lastModified[k]),f.etag[k]&&v.setRequestHeader("If-None-Match",f.etag[k])),v.setRequestHeader("Accept",d.dataTypes[0]&&d.accepts[d.dataTypes[0]]?d.accepts[d.dataTypes[0]]+(d.dataTypes[0]!=="*"?", "+bW+"; q=0.01":""):d.accepts["*"]);for(u in d.headers)v.setRequestHeader(u,d.headers[u]);if(d.beforeSend&&(d.beforeSend.call(e,v,d)===!1||s===2)){v.abort();return!1}for(u in{success:1,error:1,complete:1})v[u](d[u]);p=bZ(bT,d,c,v);if(!p)w(-1,"No Transport");else{v.readyState=1,t&&g.trigger("ajaxSend",[v,d]),d.async&&d.timeout>0&&(q=setTimeout(function(){v.abort("timeout")},d.timeout));try{s=1,p.send(l,w)}catch(z){if(s<2)w(-1,z);else throw z}}return v},param:function(a,c){var d=[],e=function(a,b){b=f.isFunction(b)?b():b,d[d.length]=encodeURIComponent(a)+"="+encodeURIComponent(b)};c===b&&(c=f.ajaxSettings.traditional);if(f.isArray(a)||a.jquery&&!f.isPlainObject(a))f.each(a,function(){e(this.name,this.value)});else for(var g in a)b_(g,a[g],c,e);return d.join("&").replace(bC,"+")}}),f.extend({active:0,lastModified:{},etag:{}});var cc=f.now(),cd=/(\=)\?(&|$)|\?\?/i;f.ajaxSetup({jsonp:"callback",jsonpCallback:function(){return f.expando+"_"+cc++}}),f.ajaxPrefilter("json jsonp",function(b,c,d){var e=typeof b.data=="string"&&/^application\/x\-www\-form\-urlencoded/.test(b.contentType);if(b.dataTypes[0]==="jsonp"||b.jsonp!==!1&&(cd.test(b.url)||e&&cd.test(b.data))){var g,h=b.jsonpCallback=f.isFunction(b.jsonpCallback)?b.jsonpCallback():b.jsonpCallback,i=a[h],j=b.url,k=b.data,l="$1"+h+"$2";b.jsonp!==!1&&(j=j.replace(cd,l),b.url===j&&(e&&(k=k.replace(cd,l)),b.data===k&&(j+=(/\?/.test(j)?"&":"?")+b.jsonp+"="+h))),b.url=j,b.data=k,a[h]=function(a){g=[a]},d.always(function(){a[h]=i,g&&f.isFunction(i)&&a[h](g[0])}),b.converters["script json"]=function(){g||f.error(h+" was not called");return g[0]},b.dataTypes[0]="json";return"script"}}),f.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/javascript|ecmascript/},converters:{"text script":function(a){f.globalEval(a);return a}}}),f.ajaxPrefilter("script",function(a){a.cache===b&&(a.cache=!1),a.crossDomain&&(a.type="GET",a.global=!1)}),f.ajaxTransport("script",function(a){if(a.crossDomain){var d,e=c.head||c.getElementsByTagName("head")[0]||c.documentElement;return{send:function(f,g){d=c.createElement("script"),d.async="async",a.scriptCharset&&(d.charset=a.scriptCharset),d.src=a.url,d.onload=d.onreadystatechange=function(a,c){if(c||!d.readyState||/loaded|complete/.test(d.readyState))d.onload=d.onreadystatechange=null,e&&d.parentNode&&e.removeChild(d),d=b,c||g(200,"success")},e.insertBefore(d,e.firstChild)},abort:function(){d&&d.onload(0,1)}}}});var ce=a.ActiveXObject?function(){for(var a in cg)cg[a](0,1)}:!1,cf=0,cg;f.ajaxSettings.xhr=a.ActiveXObject?function(){return!this.isLocal&&ch()||ci()}:ch,function(a){f.extend(f.support,{ajax:!!a,cors:!!a&&"withCredentials"in a})}(f.ajaxSettings.xhr()),f.support.ajax&&f.ajaxTransport(function(c){if(!c.crossDomain||f.support.cors){var d;return{send:function(e,g){var h=c.xhr(),i,j;c.username?h.open(c.type,c.url,c.async,c.username,c.password):h.open(c.type,c.url,c.async);if(c.xhrFields)for(j in c.xhrFields)h[j]=c.xhrFields[j];c.mimeType&&h.overrideMimeType&&h.overrideMimeType(c.mimeType),!c.crossDomain&&!e["X-Requested-With"]&&(e["X-Requested-With"]="XMLHttpRequest");try{for(j in e)h.setRequestHeader(j,e[j])}catch(k){}h.send(c.hasContent&&c.data||null),d=function(a,e){var j,k,l,m,n;try{if(d&&(e||h.readyState===4)){d=b,i&&(h.onreadystatechange=f.noop,ce&&delete cg[i]);if(e)h.readyState!==4&&h.abort();else{j=h.status,l=h.getAllResponseHeaders(),m={},n=h.responseXML,n&&n.documentElement&&(m.xml=n);try{m.text=h.responseText}catch(a){}try{k=h.statusText}catch(o){k=""}!j&&c.isLocal&&!c.crossDomain?j=m.text?200:404:j===1223&&(j=204)}}}catch(p){e||g(-1,p)}m&&g(j,k,m,l)},!c.async||h.readyState===4?d():(i=++cf,ce&&(cg||(cg={},f(a).unload(ce)),cg[i]=d),h.onreadystatechange=d)},abort:function(){d&&d(0,1)}}}});var cj={},ck,cl,cm=/^(?:toggle|show|hide)$/,cn=/^([+\-]=)?([\d+.\-]+)([a-z%]*)$/i,co,cp=[["height","marginTop","marginBottom","paddingTop","paddingBottom"],["width","marginLeft","marginRight","paddingLeft","paddingRight"],["opacity"]],cq;f.fn.extend({show:function(a,b,c){var d,e;if(a||a===0)return this.animate(ct("show",3),a,b,c);for(var g=0,h=this.length;g=i.duration+this.startTime){this.now=this.end,this.pos=this.state=1,this.update(),i.animatedProperties[this.prop]=!0;for(b in i.animatedProperties)i.animatedProperties[b]!==!0&&(g=!1);if(g){i.overflow!=null&&!f.support.shrinkWrapBlocks&&f.each(["","X","Y"],function(a,b){h.style["overflow"+b]=i.overflow[a]}),i.hide&&f(h).hide();if(i.hide||i.show)for(b in i.animatedProperties)f.style(h,b,i.orig[b]),f.removeData(h,"fxshow"+b,!0),f.removeData(h,"toggle"+b,!0);d=i.complete,d&&(i.complete=!1,d.call(h))}return!1}i.duration==Infinity?this.now=e:(c=e-this.startTime,this.state=c/i.duration,this.pos=f.easing[i.animatedProperties[this.prop]](this.state,c,0,1,i.duration),this.now=this.start+(this.end-this.start)*this.pos),this.update();return!0}},f.extend(f.fx,{tick:function(){var a,b=f.timers,c=0;for(;c-1,k={},l={},m,n;j?(l=e.position(),m=l.top,n=l.left):(m=parseFloat(h)||0,n=parseFloat(i)||0),f.isFunction(b)&&(b=b.call(a,c,g)),b.top!=null&&(k.top=b.top-g.top+m),b.left!=null&&(k.left=b.left-g.left+n),"using"in b?b.using.call(a,k):e.css(k)}},f.fn.extend({position:function(){if(!this[0])return null;var a=this[0],b=this.offsetParent(),c=this.offset(),d=cx.test(b[0].nodeName)?{top:0,left:0}:b.offset();c.top-=parseFloat(f.css(a,"marginTop"))||0,c.left-=parseFloat(f.css(a,"marginLeft"))||0,d.top+=parseFloat(f.css(b[0],"borderTopWidth"))||0,d.left+=parseFloat(f.css(b[0],"borderLeftWidth"))||0;return{top:c.top-d.top,left:c.left-d.left}},offsetParent:function(){return this.map(function(){var a=this.offsetParent||c.body;while(a&&!cx.test(a.nodeName)&&f.css(a,"position")==="static")a=a.offsetParent;return a})}}),f.each({scrollLeft:"pageXOffset",scrollTop:"pageYOffset"},function(a,c){var d=/Y/.test(c);f.fn[a]=function(e){return f.access(this,function(a,e,g){var h=cy(a);if(g===b)return h?c in h?h[c]:f.support.boxModel&&h.document.documentElement[e]||h.document.body[e]:a[e];h?h.scrollTo(d?f(h).scrollLeft():g,d?g:f(h).scrollTop()):a[e]=g},a,e,arguments.length,null)}}),f.each({Height:"height",Width:"width"},function(a,c){var d="client"+a,e="scroll"+a,g="offset"+a;f.fn["inner"+a]=function(){var a=this[0];return a?a.style?parseFloat(f.css(a,c,"padding")):this[c]():null},f.fn["outer"+a]=function(a){var b=this[0];return b?b.style?parseFloat(f.css(b,c,a?"margin":"border")):this[c]():null},f.fn[c]=function(a){return f.access(this,function(a,c,h){var i,j,k,l;if(f.isWindow(a)){i=a.document,j=i.documentElement[d];return f.support.boxModel&&j||i.body&&i.body[d]||j}if(a.nodeType===9){i=a.documentElement;if(i[d]>=i[e])return i[d];return Math.max(a.body[e],i[e],a.body[g],i[g])}if(h===b){k=f.css(a,c),l=parseFloat(k);return f.isNumeric(l)?l:k}f(a).css(c,h)},c,a,arguments.length,null)}}),a.jQuery=a.$=f,typeof define=="function"&&define.amd&&define.amd.jQuery&&define("jquery",[],function(){return f})})(window); \ No newline at end of file diff --git a/static/js/lzw.js b/static/js/lzw.js new file mode 100644 index 0000000..eac2190 --- /dev/null +++ b/static/js/lzw.js @@ -0,0 +1,236 @@ +// Author: Anthony McKale +// +// Note: modifed to javascript from orginal as2 found below +// basically identical actual to as2 +// +// +// http://www.razorberry.com/blog/archives/2004/08/22/lzw-compression-methods-in-as2/ +// +// A class for LZW compression modified from code posted at the following URL's +// http://www.shoe-box.org/blog/index.php/2004/05/05/13-CompressionLzw +// http://www.lalex.com/blog/comments/200405/164-compression-lzw-actionscript-2.html +// +var lzw = { + // Change this variable to output an xml safe string + xmlsafe : false, + compress : function(str){ + var dico = new Array(); + var skipnum = lzw.xmlsafe?5:0; + for (var i = 0; i < 256; i++) + { + dico[String.fromCharCode(i)] = i; + } + if (lzw.xmlsafe) + { + dico["<"] = 256; + dico[">"] = 257; + dico["&"] = 258; + dico["\""] = 259; + dico["'"] = 260; + } + var res = ""; + var txt2encode = str; + var splitStr = txt2encode.split(""); + var len = splitStr.length; + var nbChar = 256+skipnum; + var buffer = ""; + for (var i = 0; i <= len; i++) + { + var current = splitStr[i]; + if (dico[buffer + current] !== undefined) + { + buffer += current; + } + else + { + res += String.fromCharCode(dico[buffer]); + dico[buffer + current] = nbChar; + nbChar++; + buffer = current; + } + } + return res; + }, + decompress : function (str) + { + var dico = new Array(); + var skipnum = lzw.xmlsafe?5:0; + for (var i = 0; i < 256; i++) + { + var c = String.fromCharCode(i); + dico[i] = c; + } + if (lzw.xmlsafe) + { + dico[256] = "<"; + dico[257] = ">"; + dico[258] = "&"; + dico[259] = "\""; + dico[260] = "'"; + } + var txt2encode = str; + var splitStr = txt2encode.split(""); + var length = splitStr.length; + var nbChar = 256+skipnum; + var buffer = ""; + var chaine = ""; + var result = ""; + for (var i = 0; i < length; i++) + { + var code = txt2encode.charCodeAt(i); + var current = dico[code]; + if (buffer == "") + { + buffer = current; + result += current; + } + else + { + if (code <= 255+skipnum) + { + result += current; + chaine = buffer + current; + dico[nbChar] = chaine; + nbChar++; + buffer = current; + } + else + { + chaine = dico[code]; + if (chaine == undefined) chaine = buffer + buffer.slice(0,1); + result += chaine; + dico[nbChar] = buffer + chaine.slice(0, 1); + nbChar++; + buffer = chaine; + + } + } + } + return result; + } +} + + +// LZW-compress a string +function lzw_encode(s) { + var dict = {}; + var data = (s + "").split(""); + var out = []; + var currChar; + var phrase = data[0]; + var code = 256; + for (var i=1; i 1 ? dict[phrase] : phrase.charCodeAt(0)); + dict[phrase + currChar] = code; + code++; + phrase=currChar; + } + } + out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0)); + for (var i=0; i UTF-16 convertion + * + * Copyright (C) 1999 Masanao Izumo + * Version: 1.0 + * LastModified: Dec 25 1999 + * This library is free. You can redistribute it and/or modify it. + */ + +/* + * Interfaces: + * utf8 = utf16to8(utf16); + * utf16 = utf16to8(utf8); + */ + +function utf16to8(str) { + var out, i, len, c; + + out = ""; + len = str.length; + for(i = 0; i < len; i++) { + c = str.charCodeAt(i); + if ((c >= 0x0001) && (c <= 0x007F)) { + out += str.charAt(i); + } else if (c > 0x07FF) { + out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F)); + out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F)); + out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F)); + } else { + out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F)); + out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F)); + } + } + return out; +} + +function utf8to16(str) { + var out, i, len, c; + var char2, char3; + + out = ""; + len = str.length; + i = 0; + while(i < len) { + c = str.charCodeAt(i++); + switch(c >> 4) + { + case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: + // 0xxxxxxx + out += str.charAt(i-1); + break; + case 12: case 13: + // 110x xxxx 10xx xxxx + char2 = str.charCodeAt(i++); + out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F)); + break; + case 14: + // 1110 xxxx 10xx xxxx 10xx xxxx + char2 = str.charCodeAt(i++); + char3 = str.charCodeAt(i++); + out += String.fromCharCode(((c & 0x0F) << 12) | + ((char2 & 0x3F) << 6) | + ((char3 & 0x3F) << 0)); + break; + } + } + + return out; +} \ No newline at end of file diff --git a/views/base.tpl b/views/base.tpl index 72478d3..e112c62 100644 --- a/views/base.tpl +++ b/views/base.tpl @@ -8,7 +8,7 @@ content="0bin is a client-side-encrypted pastebin with a burn after reading feature"> - + @@ -17,8 +17,10 @@ - - + + + + @@ -82,9 +84,7 @@
- - - + "},{cN:"xmlDocTag",b:""}]},hljs.CLCM,hljs.CBLCLM,{cN:"preprocessor",b:"#",e:"$",k:{"if":1,"else":1,elif:1,endif:1,define:1,undef:1,warning:1,error:1,line:1,region:1,endregion:1,pragma:1,checksum:1}},{cN:"string",b:'@"',e:'"',c:[{b:'""'}]},hljs.ASM,hljs.QSM,hljs.CNM]}};hljs.LANGUAGES.ruby=function(){var c="[a-zA-Z_][a-zA-Z0-9_]*(\\!|\\?)?";var i="[a-zA-Z_]\\w*[!?=]?|[-+~]\\@|<<|>>|=~|===?|<=>|[<>]=?|\\*\\*|[-/+%^&*~`|]|\\[\\]=?";var a={keyword:{and:1,"false":1,then:1,defined:1,module:1,"in":1,"return":1,redo:1,"if":1,BEGIN:1,retry:1,end:1,"for":1,"true":1,self:1,when:1,next:1,until:1,"do":1,begin:1,unless:1,END:1,rescue:1,nil:1,"else":1,"break":1,undef:1,not:1,"super":1,"class":1,"case":1,require:1,yield:1,alias:1,"while":1,ensure:1,elsif:1,or:1,def:1},keymethods:{__id__:1,__send__:1,abort:1,abs:1,"all?":1,allocate:1,ancestors:1,"any?":1,arity:1,assoc:1,at:1,at_exit:1,autoload:1,"autoload?":1,"between?":1,binding:1,binmode:1,"block_given?":1,call:1,callcc:1,caller:1,capitalize:1,"capitalize!":1,casecmp:1,"catch":1,ceil:1,center:1,chomp:1,"chomp!":1,chop:1,"chop!":1,chr:1,"class":1,class_eval:1,"class_variable_defined?":1,class_variables:1,clear:1,clone:1,close:1,close_read:1,close_write:1,"closed?":1,coerce:1,collect:1,"collect!":1,compact:1,"compact!":1,concat:1,"const_defined?":1,const_get:1,const_missing:1,const_set:1,constants:1,count:1,crypt:1,"default":1,default_proc:1,"delete":1,"delete!":1,delete_at:1,delete_if:1,detect:1,display:1,div:1,divmod:1,downcase:1,"downcase!":1,downto:1,dump:1,dup:1,each:1,each_byte:1,each_index:1,each_key:1,each_line:1,each_pair:1,each_value:1,each_with_index:1,"empty?":1,entries:1,eof:1,"eof?":1,"eql?":1,"equal?":1,"eval":1,exec:1,exit:1,"exit!":1,extend:1,fail:1,fcntl:1,fetch:1,fileno:1,fill:1,find:1,find_all:1,first:1,flatten:1,"flatten!":1,floor:1,flush:1,for_fd:1,foreach:1,fork:1,format:1,freeze:1,"frozen?":1,fsync:1,getc:1,gets:1,global_variables:1,grep:1,gsub:1,"gsub!":1,"has_key?":1,"has_value?":1,hash:1,hex:1,id:1,include:1,"include?":1,included_modules:1,index:1,indexes:1,indices:1,induced_from:1,inject:1,insert:1,inspect:1,instance_eval:1,instance_method:1,instance_methods:1,"instance_of?":1,"instance_variable_defined?":1,instance_variable_get:1,instance_variable_set:1,instance_variables:1,"integer?":1,intern:1,invert:1,ioctl:1,"is_a?":1,isatty:1,"iterator?":1,join:1,"key?":1,keys:1,"kind_of?":1,lambda:1,last:1,length:1,lineno:1,ljust:1,load:1,local_variables:1,loop:1,lstrip:1,"lstrip!":1,map:1,"map!":1,match:1,max:1,"member?":1,merge:1,"merge!":1,method:1,"method_defined?":1,method_missing:1,methods:1,min:1,module_eval:1,modulo:1,name:1,nesting:1,"new":1,next:1,"next!":1,"nil?":1,nitems:1,"nonzero?":1,object_id:1,oct:1,open:1,pack:1,partition:1,pid:1,pipe:1,pop:1,popen:1,pos:1,prec:1,prec_f:1,prec_i:1,print:1,printf:1,private_class_method:1,private_instance_methods:1,"private_method_defined?":1,private_methods:1,proc:1,protected_instance_methods:1,"protected_method_defined?":1,protected_methods:1,public_class_method:1,public_instance_methods:1,"public_method_defined?":1,public_methods:1,push:1,putc:1,puts:1,quo:1,raise:1,rand:1,rassoc:1,read:1,read_nonblock:1,readchar:1,readline:1,readlines:1,readpartial:1,rehash:1,reject:1,"reject!":1,remainder:1,reopen:1,replace:1,require:1,"respond_to?":1,reverse:1,"reverse!":1,reverse_each:1,rewind:1,rindex:1,rjust:1,round:1,rstrip:1,"rstrip!":1,scan:1,seek:1,select:1,send:1,set_trace_func:1,shift:1,singleton_method_added:1,singleton_methods:1,size:1,sleep:1,slice:1,"slice!":1,sort:1,"sort!":1,sort_by:1,split:1,sprintf:1,squeeze:1,"squeeze!":1,srand:1,stat:1,step:1,store:1,strip:1,"strip!":1,sub:1,"sub!":1,succ:1,"succ!":1,sum:1,superclass:1,swapcase:1,"swapcase!":1,sync:1,syscall:1,sysopen:1,sysread:1,sysseek:1,system:1,syswrite:1,taint:1,"tainted?":1,tell:1,test:1,"throw":1,times:1,to_a:1,to_ary:1,to_f:1,to_hash:1,to_i:1,to_int:1,to_io:1,to_proc:1,to_s:1,to_str:1,to_sym:1,tr:1,"tr!":1,tr_s:1,"tr_s!":1,trace_var:1,transpose:1,trap:1,truncate:1,"tty?":1,type:1,ungetc:1,uniq:1,"uniq!":1,unpack:1,unshift:1,untaint:1,untrace_var:1,upcase:1,"upcase!":1,update:1,upto:1,"value?":1,values:1,values_at:1,warn:1,write:1,write_nonblock:1,"zero?":1,zip:1}};var d={cN:"yardoctag",b:"@[A-Za-z]+"};var k=[{cN:"comment",b:"#",e:"$",c:[d]},{cN:"comment",b:"^\\=begin",e:"^\\=end",c:[d],r:10},{cN:"comment",b:"^__END__",e:"\\n$"}];var e={cN:"subst",b:"#\\{",e:"}",l:c,k:a};var g=[hljs.BE,e];var f=[{cN:"string",b:"'",e:"'",c:g,r:0},{cN:"string",b:'"',e:'"',c:g,r:0},{cN:"string",b:"%[qw]?\\(",e:"\\)",c:g,r:10},{cN:"string",b:"%[qw]?\\[",e:"\\]",c:g,r:10},{cN:"string",b:"%[qw]?{",e:"}",c:g,r:10},{cN:"string",b:"%[qw]?<",e:">",c:g,r:10},{cN:"string",b:"%[qw]?/",e:"/",c:g,r:10},{cN:"string",b:"%[qw]?%",e:"%",c:g,r:10},{cN:"string",b:"%[qw]?-",e:"-",c:g,r:10},{cN:"string",b:"%[qw]?\\|",e:"\\|",c:g,r:10}];var h={cN:"function",b:"\\bdef\\s+",e:" |$|;",l:c,k:a,c:[{cN:"title",b:i,l:c,k:a},{cN:"params",b:"\\(",e:"\\)",l:c,k:a}].concat(k)};var j={cN:"identifier",b:c,l:c,k:a,r:0};var b=k.concat(f.concat([{cN:"class",b:"\\b(class|module)\\b",e:"$|;",k:{"class":1,module:1},c:[{cN:"title",b:"[A-Za-z_]\\w*(::\\w+)*(\\?|\\!)?",r:0},{cN:"inheritance",b:"<\\s*",c:[{cN:"parent",b:"("+hljs.IR+"::)?"+hljs.IR}]}].concat(k)},h,{cN:"constant",b:"(::)?([A-Z]\\w*(::)?)+",r:0},{cN:"symbol",b:":",c:f.concat([j]),r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{cN:"number",b:"\\?\\w"},{cN:"variable",b:"(\\$\\W)|((\\$|\\@\\@?)(\\w+))"},j,{b:"("+hljs.RSR+")\\s*",c:k.concat([{cN:"regexp",b:"/",e:"/[a-z]*",i:"\\n",c:[hljs.BE]}]),r:0}]));e.c=b;h.c[1].c=b;return{dM:{l:c,k:a,c:b}}}();hljs.LANGUAGES.rust=function(){var c={cN:"title",b:hljs.UIR};var d={cN:"string",b:'"',e:'"',c:[hljs.BE],r:0};var b={cN:"number",b:"\\b(0[xb][A-Za-z0-9_]+|[0-9_]+(\\.[0-9_]+)?([uif](8|16|32|64)?)?)",r:0};var a={alt:1,any:1,as:1,assert:1,be:1,bind:1,block:1,bool:1,"break":1,"char":1,check:1,claim:1,"const":1,cont:1,dir:1,"do":1,"else":1,"enum":1,"export":1,f32:1,f64:1,fail:1,"false":1,"float":1,fn:10,"for":1,i16:1,i32:1,i64:1,i8:1,"if":1,iface:10,impl:10,"import":1,"in":1,"int":1,let:1,log:1,mod:1,mutable:1,"native":1,note:1,of:1,prove:1,pure:10,resource:1,ret:1,self:1,str:1,syntax:1,"true":1,type:1,u16:1,u32:1,u64:1,u8:1,uint:1,unchecked:1,unsafe:1,use:1,vec:1,"while":1};return{dM:{k:a,i:"]+"}]}]};return{cI:true,dM:{c:[{cN:"pi",b:"<\\?",e:"\\?>",r:10},{cN:"doctype",b:"",r:10,c:[{b:"\\[",e:"\\]"}]},{cN:"comment",b:"",r:10},{cN:"cdata",b:"<\\!\\[CDATA\\[",e:"\\]\\]>",r:10},{cN:"tag",b:"|$)",e:">",k:{title:{style:1}},c:[a],starts:{cN:"css",e:"",rE:true,sL:"css"}},{cN:"tag",b:"|$)",e:">",k:{title:{script:1}},c:[a],starts:{cN:"javascript",e:"<\/script>",rE:true,sL:"javascript"}},{cN:"vbscript",b:"<%",e:"%>",sL:"vbscript"},{cN:"tag",b:"",c:[{cN:"title",b:"[^ />]+"},a]}]}}}();hljs.LANGUAGES.markdown={cI:true,dM:{c:[{cN:"header",b:"^#{1,3}",e:"$"},{cN:"header",b:"^.+?\\n[=-]{2,}$"},{b:"<",e:">",sL:"xml"},{cN:"bullet",b:"^([*+-]|(\\d+\\.))\\s+"},{cN:"strong",b:"[*_]{2}.+?[*_]{2}"},{cN:"emphasis",b:"[*_].+?[*_]"},{cN:"blockquote",b:"^>\\s+",e:"$"},{cN:"code",b:"`.+?`"},{cN:"code",b:"^ ",e:"$",r:0},{cN:"horizontal_rule",b:"^-{3,}",e:"$"},{b:"\\[.+?\\]\\(.+?\\)",rB:true,c:[{cN:"link_label",b:"\\[.+\\]"},{cN:"link_url",b:"\\(",e:"\\)",eB:true,eE:true}]}]}};hljs.LANGUAGES.css=function(){var a={cN:"function",b:hljs.IR+"\\(",e:"\\)",c:[{eW:true,eE:true,c:[hljs.NM,hljs.ASM,hljs.QSM]}]};return{cI:true,dM:{i:"[=/|']",c:[hljs.CBLCLM,{cN:"id",b:"\\#[A-Za-z0-9_-]+"},{cN:"class",b:"\\.[A-Za-z0-9_-]+",r:0},{cN:"attr_selector",b:"\\[",e:"\\]",i:"$"},{cN:"pseudo",b:":(:)?[a-zA-Z0-9\\_\\-\\+\\(\\)\\\"\\']+"},{cN:"at_rule",b:"@(font-face|page)",l:"[a-z-]+",k:{"font-face":1,page:1}},{cN:"at_rule",b:"@",e:"[{;]",eE:true,k:{"import":1,page:1,media:1,charset:1},c:[a,hljs.ASM,hljs.QSM,hljs.NM]},{cN:"tag",b:hljs.IR,r:0},{cN:"rules",b:"{",e:"}",i:"[^\\s]",r:0,c:[hljs.CBLCLM,{cN:"rule",b:"[^\\s]",rB:true,e:";",eW:true,c:[{cN:"attribute",b:"[A-Z\\_\\.\\-]+",e:":",eE:true,i:"[^\\s]",starts:{cN:"value",eW:true,eE:true,c:[a,hljs.NM,hljs.QSM,hljs.ASM,hljs.CBLCLM,{cN:"hexcolor",b:"\\#[0-9A-F]+"},{cN:"important",b:"!important"}]}}]}]}]}}}();hljs.LANGUAGES.lisp=function(){var k="[a-zA-Z_\\-\\+\\*\\/\\<\\=\\>\\&\\#][a-zA-Z0-9_\\-\\+\\*\\/\\<\\=\\>\\&\\#]*";var m="(\\-|\\+)?\\d+(\\.\\d+|\\/\\d+)?((d|e|f|l|s)(\\+|\\-)?\\d+)?";var a={cN:"literal",b:"\\b(t{1}|nil)\\b"};var f=[{cN:"number",b:m},{cN:"number",b:"#b[0-1]+(/[0-1]+)?"},{cN:"number",b:"#o[0-7]+(/[0-7]+)?"},{cN:"number",b:"#x[0-9a-f]+(/[0-9a-f]+)?"},{cN:"number",b:"#c\\("+m+" +"+m,e:"\\)"}];var b={cN:"string",b:'"',e:'"',c:[hljs.BE],r:0};var l={cN:"comment",b:";",e:"$"};var c={cN:"variable",b:"\\*",e:"\\*"};var j={cN:"keyword",b:"[:&]"+k};var h={b:"\\(",e:"\\)",c:["self",a,b].concat(f)};var e={cN:"quoted",b:"['`]\\(",e:"\\)",c:f.concat([b,c,j,h])};var d={cN:"quoted",b:"\\(quote ",e:"\\)",k:{title:{quote:1}},c:f.concat([b,c,j,h])};var i={cN:"list",b:"\\(",e:"\\)"};var g={cN:"body",eW:true,eE:true};i.c=[{cN:"title",b:k},g];g.c=[e,d,i,a].concat(f).concat([b,l,c,j]);return{cI:true,dM:{i:"[^\\s]",c:f.concat([a,b,l,e,d,i])}}}();hljs.LANGUAGES.profile={dM:{c:[hljs.CNM,{cN:"builtin",b:"{",e:"}$",eB:true,eE:true,c:[hljs.ASM,hljs.QSM],r:0},{cN:"filename",b:"[a-zA-Z_][\\da-zA-Z_]+\\.[\\da-zA-Z_]{1,3}",e:":",eE:true},{cN:"header",b:"(ncalls|tottime|cumtime)",e:"$",k:{ncalls:1,tottime:10,cumtime:10,filename:1},r:10},{cN:"summary",b:"function calls",e:"$",c:[hljs.CNM],r:10},hljs.ASM,hljs.QSM,{cN:"function",b:"\\(",e:"\\)$",c:[{cN:"title",b:hljs.UIR,r:0}],r:0}]}};hljs.LANGUAGES.java={dM:{k:{"false":1,"synchronized":1,"int":1,"abstract":1,"float":1,"private":1,"char":1,"interface":1,"boolean":1,"static":1,"null":1,"if":1,"const":1,"for":1,"true":1,"while":1,"long":1,"throw":1,strictfp:1,"finally":1,"protected":1,"extends":1,"import":1,"native":1,"final":1,"implements":1,"return":1,"void":1,"enum":1,"else":1,"break":1,"transient":1,"new":1,"catch":1,"instanceof":1,"byte":1,"super":1,"class":1,"volatile":1,"case":1,assert:1,"short":1,"package":1,"default":1,"double":1,"public":1,"try":1,"this":1,"switch":1,"continue":1,"throws":1},c:[{cN:"javadoc",b:"/\\*\\*",e:"\\*/",c:[{cN:"javadoctag",b:"@[A-Za-z]+"}],r:10},hljs.CLCM,hljs.CBLCLM,hljs.ASM,hljs.QSM,{cN:"class",b:"(class |interface )",e:"{",k:{"class":1,"interface":1},i:":",c:[{b:"(implements|extends)",k:{"extends":1,"implements":1},r:10},{cN:"title",b:hljs.UIR}]},hljs.CNM,{cN:"annotation",b:"@[A-Za-z]+"}]}};hljs.LANGUAGES.php={cI:true,dM:{k:{and:1,include_once:1,list:1,"abstract":1,global:1,"private":1,echo:1,"interface":1,as:1,"static":1,endswitch:1,array:1,"null":1,"if":1,endwhile:1,or:1,"const":1,"for":1,endforeach:1,self:1,"var":1,"while":1,isset:1,"public":1,"protected":1,exit:1,foreach:1,"throw":1,elseif:1,"extends":1,include:1,__FILE__:1,empty:1,require_once:1,"function":1,"do":1,xor:1,"return":1,"implements":1,parent:1,clone:1,use:1,__CLASS__:1,__LINE__:1,"else":1,"break":1,print:1,"eval":1,"new":1,"catch":1,__METHOD__:1,"class":1,"case":1,exception:1,php_user_filter:1,"default":1,die:1,require:1,__FUNCTION__:1,enddeclare:1,"final":1,"try":1,"this":1,"switch":1,"continue":1,endfor:1,endif:1,declare:1,unset:1,"true":1,"false":1,namespace:1,trait:1,"goto":1,"instanceof":1,__DIR__:1,__NAMESPACE__:1,__halt_compiler:1},c:[hljs.CLCM,hljs.HCM,{cN:"comment",b:"/\\*",e:"\\*/",c:[{cN:"phpdoc",b:"\\s@[A-Za-z]+"}]},{cN:"comment",eB:true,b:"__halt_compiler[^;]+;",e:"[\\n\\r]$"},hljs.CNM,hljs.BINARY_NUMBER_MODE,hljs.inherit(hljs.ASM,{i:null}),hljs.inherit(hljs.QSM,{i:null}),{cN:"string",b:'b"',e:'"',c:[hljs.BE]},{cN:"string",b:"b'",e:"'",c:[hljs.BE]},{cN:"string",b:"<<<['\"]?\\w+['\"]?$",e:"^\\w+;",c:[hljs.BE]},{cN:"variable",b:"\\$+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*"},{cN:"preprocessor",b:"<\\?php",r:10},{cN:"preprocessor",b:"\\?>"}]}};hljs.LANGUAGES.haskell=function(){var a={cN:"label",b:"\\b[A-Z][\\w']*",r:0};var b={cN:"container",b:"\\(",e:"\\)",c:[{cN:"label",b:"\\b[A-Z][\\w\\(\\)\\.']*"},{cN:"title",b:"[_a-z][\\w']*"}]};return{dM:{k:{keyword:{let:1,"in":1,"if":1,then:1,"else":1,"case":1,of:1,where:1,"do":1,module:1,"import":1,hiding:1,qualified:1,type:1,data:1,newtype:1,deriving:1,"class":1,instance:1,"null":1,not:1,as:1}},c:[{cN:"comment",b:"--",e:"$"},{cN:"comment",b:"{-",e:"-}"},{cN:"string",b:"\\s+'",e:"'",c:[hljs.BE],r:0},hljs.QSM,{cN:"import",b:"\\bimport",e:"$",k:{"import":1,qualified:1,as:1,hiding:1},c:[b]},{cN:"module",b:"\\bmodule",e:"where",k:{module:1,where:1},c:[b]},{cN:"class",b:"\\b(class|instance|data|(new)?type)",e:"(where|$)",k:{"class":1,where:1,instance:1,data:1,type:1,newtype:1,deriving:1},c:[a]},hljs.CNM,{cN:"shebang",b:"#!\\/usr\\/bin\\/env runhaskell",e:"$"},a,{cN:"title",b:"^[_a-z][\\w']*"}]}}}();hljs.LANGUAGES["1c"]=function(){var b="[a-zA-Zа-яА-Я][a-zA-Z0-9_а-яА-Я]*";var e={"возврат":1,"дата":1,"для":1,"если":1,"и":1,"или":1,"иначе":1,"иначеесли":1,"исключение":1,"конецесли":1,"конецпопытки":1,"конецпроцедуры":1,"конецфункции":1,"конеццикла":1,"константа":1,"не":1,"перейти":1,"перем":1,"перечисление":1,"по":1,"пока":1,"попытка":1,"прервать":1,"продолжить":1,"процедура":1,"строка":1,"тогда":1,"фс":1,"функция":1,"цикл":1,"число":1,"экспорт":1};var d={ansitooem:1,oemtoansi:1,"ввестивидсубконто":1,"ввестидату":1,"ввестизначение":1,"ввестиперечисление":1,"ввестипериод":1,"ввестиплансчетов":1,"ввестистроку":1,"ввестичисло":1,"вопрос":1,"восстановитьзначение":1,"врег":1,"выбранныйплансчетов":1,"вызватьисключение":1,"датагод":1,"датамесяц":1,"датачисло":1,"добавитьмесяц":1,"завершитьработусистемы":1,"заголовоксистемы":1,"записьжурналарегистрации":1,"запуститьприложение":1,"зафиксироватьтранзакцию":1,"значениевстроку":1,"значениевстрокувнутр":1,"значениевфайл":1,"значениеизстроки":1,"значениеизстрокивнутр":1,"значениеизфайла":1,"имякомпьютера":1,"имяпользователя":1,"каталогвременныхфайлов":1,"каталогиб":1,"каталогпользователя":1,"каталогпрограммы":1,"кодсимв":1,"командасистемы":1,"конгода":1,"конецпериодаби":1,"конецрассчитанногопериодаби":1,"конецстандартногоинтервала":1,"конквартала":1,"конмесяца":1,"коннедели":1,"лев":1,"лог":1,"лог10":1,"макс":1,"максимальноеколичествосубконто":1,"мин":1,"монопольныйрежим":1,"названиеинтерфейса":1,"названиенабораправ":1,"назначитьвид":1,"назначитьсчет":1,"найти":1,"найтипомеченныенаудаление":1,"найтиссылки":1,"началопериодаби":1,"началостандартногоинтервала":1,"начатьтранзакцию":1,"начгода":1,"начквартала":1,"начмесяца":1,"начнедели":1,"номерднягода":1,"номерднянедели":1,"номернеделигода":1,"нрег":1,"обработкаожидания":1,"окр":1,"описаниеошибки":1,"основнойжурналрасчетов":1,"основнойплансчетов":1,"основнойязык":1,"открытьформу":1,"открытьформумодально":1,"отменитьтранзакцию":1,"очиститьокносообщений":1,"периодстр":1,"полноеимяпользователя":1,"получитьвремята":1,"получитьдатута":1,"получитьдокументта":1,"получитьзначенияотбора":1,"получитьпозициюта":1,"получитьпустоезначение":1,"получитьта":1,"прав":1,"праводоступа":1,"предупреждение":1,"префиксавтонумерации":1,"пустаястрока":1,"пустоезначение":1,"рабочаядаттьпустоезначение":1,"рабочаядата":1,"разделительстраниц":1,"разделительстрок":1,"разм":1,"разобратьпозициюдокумента":1,"рассчитатьрегистрына":1,"рассчитатьрегистрыпо":1,"сигнал":1,"симв":1,"символтабуляции":1,"создатьобъект":1,"сокрл":1,"сокрлп":1,"сокрп":1," сообщить":1,"состояние":1,"сохранитьзначение":1,"сред":1,"статусвозврата":1,"стрдлина":1,"стрзаменить":1,"стрколичествострок":1,"стрполучитьстроку":1," стрчисловхождений":1,"сформироватьпозициюдокумента":1,"счетпокоду":1,"текущаядата":1,"текущеевремя":1,"типзначения":1,"типзначениястр":1,"удалитьобъекты":1,"установитьтана":1,"установитьтапо":1,"фиксшаблон":1,"формат":1,"цел":1,"шаблон":1};var a={cN:"dquote",b:'""'};var c={cN:"string",b:'"',e:'"|$',c:[a],r:0};var f={cN:"string",b:"\\|",e:'"|$',c:[a]};return{cI:true,dM:{l:b,k:{keyword:e,built_in:d},c:[hljs.CLCM,hljs.NM,c,f,{cN:"function",b:"(процедура|функция)",e:"$",l:b,k:{"процедура":1,"экспорт":1,"функция":1},c:[{cN:"title",b:b},{cN:"tail",eW:true,c:[{cN:"params",b:"\\(",e:"\\)",l:b,k:{"знач":1},c:[c,f]},{cN:"export",b:"экспорт",eW:true,l:b,k:{"экспорт":1},c:[hljs.CLCM]}]},hljs.CLCM]},{cN:"preprocessor",b:"#",e:"$"},{cN:"date",b:"'\\d{2}\\.\\d{2}\\.(\\d{2}|\\d{4})'"}]}}}();hljs.LANGUAGES.python=function(){var a=[{cN:"string",b:"(u|b)?r?'''",e:"'''",r:10},{cN:"string",b:'(u|b)?r?"""',e:'"""',r:10},{cN:"string",b:"(u|r|ur)'",e:"'",c:[hljs.BE],r:10},{cN:"string",b:'(u|r|ur)"',e:'"',c:[hljs.BE],r:10},{cN:"string",b:"(b|br)'",e:"'",c:[hljs.BE]},{cN:"string",b:'(b|br)"',e:'"',c:[hljs.BE]}].concat([hljs.ASM,hljs.QSM]);var b={cN:"title",b:hljs.UIR};var c={cN:"params",b:"\\(",e:"\\)",c:a.concat([hljs.CNM])};return{dM:{k:{keyword:{and:1,elif:1,is:1,global:1,as:1,"in":1,"if":1,from:1,raise:1,"for":1,except:1,"finally":1,print:1,"import":1,pass:1,"return":1,exec:1,"else":1,"break":1,not:1,"with":1,"class":1,assert:1,yield:1,"try":1,"while":1,"continue":1,del:1,or:1,def:1,lambda:1,nonlocal:10},built_in:{None:1,True:1,False:1,Ellipsis:1,NotImplemented:1}},i:"(|\\?)",c:a.concat([hljs.HCM,{cN:"function",b:"\\bdef ",e:":",i:"$",k:{def:1},c:[b,c],r:10},{cN:"class",b:"\\bclass ",e:":",i:"[${]",k:{"class":1},c:[b,c],r:10},hljs.CNM,{cN:"decorator",b:"@",e:"$"}])}}}();hljs.LANGUAGES.smalltalk=function(){var b="[a-z][a-zA-Z0-9_]*";var c={cN:"char",b:"\\$.{1}"};var a={cN:"symbol",b:"#"+hljs.UIR};return{dM:{k:{self:1,"super":1,nil:1,"true":1,"false":1,thisContext:1},c:[{cN:"comment",b:'"',e:'"',r:0},hljs.ASM,{cN:"class",b:"\\b[A-Z][A-Za-z0-9_]*",r:0},{cN:"method",b:b+":"},hljs.CNM,a,c,{cN:"localvars",b:"\\|\\s*(("+b+")\\s*)+\\|"},{cN:"array",b:"\\#\\(",e:"\\)",c:[hljs.ASM,c,hljs.CNM,a]}]}}}();hljs.LANGUAGES.tex=function(){var c={cN:"command",b:"\\\\[a-zA-Zа-яА-я]+[\\*]?",r:10};var b={cN:"command",b:"\\\\[^a-zA-Zа-яА-я0-9]",r:0};var a={cN:"special",b:"[{}\\[\\]\\&#~]",r:0};return{dM:{c:[{b:"\\\\[a-zA-Zа-яА-я]+[\\*]? *= *-?\\d*\\.?\\d+(pt|pc|mm|cm|in|dd|cc|ex|em)?",rB:true,c:[c,b,{cN:"number",b:" *=",e:"-?\\d*\\.?\\d+(pt|pc|mm|cm|in|dd|cc|ex|em)?",eB:true}],r:10},c,b,a,{cN:"formula",b:"\\$\\$",e:"\\$\\$",c:[c,b,a],r:0},{cN:"formula",b:"\\$",e:"\\$",c:[c,b,a],r:0},{cN:"comment",b:"%",e:"$",r:0}]}}}();hljs.LANGUAGES.actionscript=function(){var c="[a-zA-Z_$][a-zA-Z0-9_$]*";var a="([*]|[a-zA-Z_$][a-zA-Z0-9_$]*)";var d={cN:"rest_arg",b:"[.]{3}",e:c,r:10};var b={cN:"title",b:c};return{dM:{k:{keyword:{as:1,"break":1,"case":1,"catch":1,"class":1,"const":1,"continue":1,"default":1,"delete":1,"do":1,dynamic:5,each:1,"else":1,"extends":1,"final":1,"finally":1,"for":1,"function":1,get:1,"if":1,"implements":1,"import":1,"in":1,include:1,"instanceof":1,"interface":1,internal:1,is:1,namespace:1,"native":1,"new":1,override:1,"package":1,"private":1,"protected":1,"public":1,"return":1,set:1,"static":1,"super":5,"switch":1,"this":1,"throw":1,"try":1,"typeof":1,use:1,"var":1,"void":1,"while":1,"with":1},literal:{"true":1,"false":1,"null":1,"undefined":1},reserved:{"abstract":0,"boolean":0,"byte":0,cast:0,"char":0,"debugger":0,"double":0,"enum":0,"export":0,"float":0,"goto":0,intrinsic:0,"long":0,prototype:0,"short":0,"synchronized":0,"throws":0,to:0,"transient":0,type:0,virtual:0,"volatile":0}},c:[hljs.ASM,hljs.QSM,hljs.CLCM,hljs.CBLCLM,hljs.CNM,{cN:"package",b:"package ?",e:"{",k:{"package":1},c:[b]},{cN:"class",b:"(class|interface) ",e:"{",k:{"class":1,"interface":1},c:[{b:"(implements|extends)",k:{"extends":1,"implements":1},r:5},b]},{cN:"preprocessor",b:"(import|include)\\b",e:";",k:{"import":1,include:1}},{cN:"function",b:"function ",e:"[{;]",k:{"function":1},c:[b,{cN:"params",b:"\\(",e:"\\)",c:[hljs.ASM,hljs.QSM,hljs.CLCM,hljs.CBLCLM,d]},{cN:"type",b:":",e:a,r:10}]}]}}}();hljs.LANGUAGES.sql={cI:true,dM:{i:"[^\\s]",c:[{cN:"operator",b:"(begin|start|commit|rollback|savepoint|lock|alter|create|drop|rename|call|delete|do|handler|insert|load|replace|select|truncate|update|set|show|pragma|grant)\\b",e:";|$",k:{keyword:{all:1,partial:1,global:1,month:1,current_timestamp:1,using:1,go:1,revoke:1,smallint:1,indicator:1,"end-exec":1,disconnect:1,zone:1,"with":1,character:1,assertion:1,to:1,add:1,current_user:1,usage:1,input:1,local:1,alter:1,match:1,collate:1,real:1,then:1,rollback:1,get:1,read:1,timestamp:1,session_user:1,not:1,integer:1,bit:1,unique:1,day:1,minute:1,desc:1,insert:1,execute:1,like:1,ilike:2,level:1,decimal:1,drop:1,"continue":1,isolation:1,found:1,where:1,constraints:1,domain:1,right:1,national:1,some:1,module:1,transaction:1,relative:1,second:1,connect:1,escape:1,close:1,system_user:1,"for":1,deferred:1,section:1,cast:1,current:1,sqlstate:1,allocate:1,intersect:1,deallocate:1,numeric:1,"public":1,preserve:1,full:1,"goto":1,initially:1,asc:1,no:1,key:1,output:1,collation:1,group:1,by:1,union:1,session:1,both:1,last:1,language:1,constraint:1,column:1,of:1,space:1,foreign:1,deferrable:1,prior:1,connection:1,unknown:1,action:1,commit:1,view:1,or:1,first:1,into:1,"float":1,year:1,primary:1,cascaded:1,except:1,restrict:1,set:1,references:1,names:1,table:1,outer:1,open:1,select:1,size:1,are:1,rows:1,from:1,prepare:1,distinct:1,leading:1,create:1,only:1,next:1,inner:1,authorization:1,schema:1,corresponding:1,option:1,declare:1,precision:1,immediate:1,"else":1,timezone_minute:1,external:1,varying:1,translation:1,"true":1,"case":1,exception:1,join:1,hour:1,"default":1,"double":1,scroll:1,value:1,cursor:1,descriptor:1,values:1,dec:1,fetch:1,procedure:1,"delete":1,and:1,"false":1,"int":1,is:1,describe:1,"char":1,as:1,at:1,"in":1,varchar:1,"null":1,trailing:1,any:1,absolute:1,current_time:1,end:1,grant:1,privileges:1,when:1,cross:1,check:1,write:1,current_date:1,pad:1,begin:1,temporary:1,exec:1,time:1,update:1,catalog:1,user:1,sql:1,date:1,on:1,identity:1,timezone_hour:1,natural:1,whenever:1,interval:1,work:1,order:1,cascade:1,diagnostics:1,nchar:1,having:1,left:1,call:1,"do":1,handler:1,load:1,replace:1,truncate:1,start:1,lock:1,show:1,pragma:1},aggregate:{count:1,sum:1,min:1,max:1,avg:1}},c:[{cN:"string",b:"'",e:"'",c:[hljs.BE,{b:"''"}],r:0},{cN:"string",b:'"',e:'"',c:[hljs.BE,{b:'""'}],r:0},{cN:"string",b:"`",e:"`",c:[hljs.BE]},hljs.CNM,{b:"\\n"}]},hljs.CBLCLM,{cN:"comment",b:"--",e:"$"}]}};hljs.LANGUAGES.vala={dM:{k:{keyword:{"char":1,uchar:1,unichar:1,"int":1,uint:1,"long":1,ulong:1,"short":1,ushort:1,int8:1,int16:1,int32:1,int64:1,uint8:1,uint16:1,uint32:1,uint64:1,"float":1,"double":1,bool:1,struct:1,"enum":1,string:1,"void":1,weak:5,unowned:5,owned:5,async:5,signal:5,"static":1,"abstract":1,"interface":1,override:1,"while":1,"do":1,"for":1,foreach:1,"else":1,"switch":1,"case":1,"break":1,"default":1,"return":1,"try":1,"catch":1,"public":1,"private":1,"protected":1,internal:1,using:1,"new":1,"this":1,get:1,set:1,"const":1,stdout:1,stdin:1,stderr:1,"var":1,DBus:2,GLib:2,CCode:10,Gee:10,Object:1},literal:{"false":1,"true":1,"null":1}},c:[{cN:"class",b:"(class |interface |delegate |namespace )",e:"{",k:{"class":1,"interface":1},c:[{b:"(implements|extends)",k:{"extends":1,"implements":1}},{cN:"title",b:hljs.UIR}]},hljs.CLCM,hljs.CBLCLM,{cN:"string",b:'"""',e:'"""',r:5},hljs.ASM,hljs.QSM,hljs.CNM,{cN:"preprocessor",b:"^#",e:"$",r:2},{cN:"constant",b:" [A-Z_]+ ",r:0}]}};hljs.LANGUAGES.ini={cI:true,dM:{i:"[^\\s]",c:[{cN:"comment",b:";",e:"$"},{cN:"title",b:"^\\[",e:"\\]"},{cN:"setting",b:"^[a-z0-9_\\[\\]]+[ \\t]*=[ \\t]*",e:"$",c:[{cN:"value",eW:true,k:{on:1,off:1,"true":1,"false":1,yes:1,no:1},c:[hljs.QSM,hljs.NM]}]}]}};hljs.LANGUAGES.axapta={dM:{k:{"false":1,"int":1,"abstract":1,"private":1,"char":1,"interface":1,"boolean":1,"static":1,"null":1,"if":1,"for":1,"true":1,"while":1,"long":1,"throw":1,"finally":1,"protected":1,"extends":1,"final":1,"implements":1,"return":1,"void":1,"enum":1,"else":1,"break":1,"new":1,"catch":1,"byte":1,"super":1,"class":1,"case":1,"short":1,"default":1,"double":1,"public":1,"try":1,"this":1,"switch":1,"continue":1,reverse:1,firstfast:1,firstonly:1,forupdate:1,nofetch:1,sum:1,avg:1,minof:1,maxof:1,count:1,order:1,group:1,by:1,asc:1,desc:1,index:1,hint:1,like:1,dispaly:1,edit:1,client:1,server:1,ttsbegin:1,ttscommit:1,str:1,real:1,date:1,container:1,anytype:1,common:1,div:1,mod:1},c:[hljs.CLCM,hljs.CBLCLM,hljs.ASM,hljs.QSM,hljs.CNM,{cN:"preprocessor",b:"#",e:"$"},{cN:"class",b:"(class |interface )",e:"{",i:":",k:{"class":1,"interface":1},c:[{cN:"inheritance",b:"(implements|extends)",k:{"extends":1,"implements":1},r:10},{cN:"title",b:hljs.UIR}]}]}};hljs.LANGUAGES.perl=function(){var c={getpwent:1,getservent:1,quotemeta:1,msgrcv:1,scalar:1,kill:1,dbmclose:1,undef:1,lc:1,ma:1,syswrite:1,tr:1,send:1,umask:1,sysopen:1,shmwrite:1,vec:1,qx:1,utime:1,local:1,oct:1,semctl:1,localtime:1,readpipe:1,"do":1,"return":1,format:1,read:1,sprintf:1,dbmopen:1,pop:1,getpgrp:1,not:1,getpwnam:1,rewinddir:1,qq:1,fileno:1,qw:1,endprotoent:1,wait:1,sethostent:1,bless:1,s:1,opendir:1,"continue":1,each:1,sleep:1,endgrent:1,shutdown:1,dump:1,chomp:1,connect:1,getsockname:1,die:1,socketpair:1,close:1,flock:1,exists:1,index:1,shmget:1,sub:1,"for":1,endpwent:1,redo:1,lstat:1,msgctl:1,setpgrp:1,abs:1,exit:1,select:1,print:1,ref:1,gethostbyaddr:1,unshift:1,fcntl:1,syscall:1,"goto":1,getnetbyaddr:1,join:1,gmtime:1,symlink:1,semget:1,splice:1,x:1,getpeername:1,recv:1,log:1,setsockopt:1,cos:1,last:1,reverse:1,gethostbyname:1,getgrnam:1,study:1,formline:1,endhostent:1,times:1,chop:1,length:1,gethostent:1,getnetent:1,pack:1,getprotoent:1,getservbyname:1,rand:1,mkdir:1,pos:1,chmod:1,y:1,substr:1,endnetent:1,printf:1,next:1,open:1,msgsnd:1,readdir:1,use:1,unlink:1,getsockopt:1,getpriority:1,rindex:1,wantarray:1,hex:1,system:1,getservbyport:1,endservent:1,"int":1,chr:1,untie:1,rmdir:1,prototype:1,tell:1,listen:1,fork:1,shmread:1,ucfirst:1,setprotoent:1,"else":1,sysseek:1,link:1,getgrgid:1,shmctl:1,waitpid:1,unpack:1,getnetbyname:1,reset:1,chdir:1,grep:1,split:1,require:1,caller:1,lcfirst:1,until:1,warn:1,"while":1,values:1,shift:1,telldir:1,getpwuid:1,my:1,getprotobynumber:1,"delete":1,and:1,sort:1,uc:1,defined:1,srand:1,accept:1,"package":1,seekdir:1,getprotobyname:1,semop:1,our:1,rename:1,seek:1,"if":1,q:1,chroot:1,sysread:1,setpwent:1,no:1,crypt:1,getc:1,chown:1,sqrt:1,write:1,setnetent:1,setpriority:1,foreach:1,tie:1,sin:1,msgget:1,map:1,stat:1,getlogin:1,unless:1,elsif:1,truncate:1,exec:1,keys:1,glob:1,tied:1,closedir:1,ioctl:1,socket:1,readlink:1,"eval":1,xor:1,readline:1,binmode:1,setservent:1,eof:1,ord:1,bind:1,alarm:1,pipe:1,atan2:1,getgrent:1,exp:1,time:1,push:1,setgrent:1,gt:1,lt:1,or:1,ne:1,m:1};var e={cN:"subst",b:"[$@]\\{",e:"\\}",k:c,r:10};var b={cN:"variable",b:"\\$\\d"};var a={cN:"variable",b:"[\\$\\%\\@\\*](\\^\\w\\b|#\\w+(\\:\\:\\w+)*|[^\\s\\w{]|{\\w+}|\\w+(\\:\\:\\w*)*)"};var h=[hljs.BE,e,b,a];var g={b:"->",c:[{b:hljs.IR},{b:"{",e:"}"}]};var d={cN:"comment",b:"^(__END__|__DATA__)",e:"\\n$",r:5};var f=[b,a,hljs.HCM,d,g,{cN:"string",b:"q[qwxr]?\\s*\\(",e:"\\)",c:h,r:5},{cN:"string",b:"q[qwxr]?\\s*\\[",e:"\\]",c:h,r:5},{cN:"string",b:"q[qwxr]?\\s*\\{",e:"\\}",c:h,r:5},{cN:"string",b:"q[qwxr]?\\s*\\|",e:"\\|",c:h,r:5},{cN:"string",b:"q[qwxr]?\\s*\\<",e:"\\>",c:h,r:5},{cN:"string",b:"qw\\s+q",e:"q",c:h,r:5},{cN:"string",b:"'",e:"'",c:[hljs.BE],r:0},{cN:"string",b:'"',e:'"',c:h,r:0},{cN:"string",b:"`",e:"`",c:[hljs.BE]},{cN:"string",b:"{\\w+}",r:0},{cN:"string",b:"-?\\w+\\s*\\=\\>",r:0},{cN:"number",b:"(\\b0[0-7_]+)|(\\b0x[0-9a-fA-F_]+)|(\\b[1-9][0-9_]*(\\.[0-9_]+)?)|[0_]\\b",r:0},{b:"("+hljs.RSR+"|split|return|print|reverse|grep)\\s*",k:{split:1,"return":1,print:1,reverse:1,grep:1},r:0,c:[hljs.HCM,d,{cN:"regexp",b:"(s|tr|y)/(\\\\.|[^/])*/(\\\\.|[^/])*/[a-z]*",r:10},{cN:"regexp",b:"(m|qr)?/",e:"/[a-z]*",c:[hljs.BE],r:0}]},{cN:"sub",b:"\\bsub\\b",e:"(\\s*\\(.*?\\))?[;{]",k:{sub:1},r:5},{cN:"operator",b:"-\\w\\b",r:0},{cN:"pod",b:"\\=\\w",e:"\\=cut"}];e.c=f;g.c[1].c=f;return{dM:{k:c,c:f}}}();hljs.LANGUAGES.scala=function(){var a={cN:"annotation",b:"@[A-Za-z]+"};var b={cN:"string",b:'u?r?"""',e:'"""',r:10};return{dM:{k:{type:1,yield:1,lazy:1,override:1,def:1,"with":1,val:1,"var":1,"false":1,"true":1,sealed:1,"abstract":1,"private":1,trait:1,object:1,"null":1,"if":1,"for":1,"while":1,"throw":1,"finally":1,"protected":1,"extends":1,"import":1,"final":1,"return":1,"else":1,"break":1,"new":1,"catch":1,"super":1,"class":1,"case":1,"package":1,"default":1,"try":1,"this":1,match:1,"continue":1,"throws":1},c:[{cN:"javadoc",b:"/\\*\\*",e:"\\*/",c:[{cN:"javadoctag",b:"@[A-Za-z]+"}],r:10},hljs.CLCM,hljs.CBLCLM,hljs.ASM,hljs.QSM,b,{cN:"class",b:"((case )?class |object |trait )",e:"({|$)",i:":",k:{"case":1,"class":1,trait:1,object:1},c:[{b:"(extends|with)",k:{"extends":1,"with":1},r:10},{cN:"title",b:hljs.UIR},{cN:"params",b:"\\(",e:"\\)",c:[hljs.ASM,hljs.QSM,b,a]}]},hljs.CNM,a]}}}();hljs.LANGUAGES.cmake={cI:true,dM:{k:{add_custom_command:2,add_custom_target:2,add_definitions:2,add_dependencies:2,add_executable:2,add_library:2,add_subdirectory:2,add_test:2,aux_source_directory:2,"break":1,build_command:2,cmake_minimum_required:3,cmake_policy:3,configure_file:1,create_test_sourcelist:1,define_property:1,"else":1,elseif:1,enable_language:2,enable_testing:2,endforeach:1,endfunction:1,endif:1,endmacro:1,endwhile:1,execute_process:2,"export":1,find_file:1,find_library:2,find_package:2,find_path:1,find_program:1,fltk_wrap_ui:2,foreach:1,"function":1,get_cmake_property:3,get_directory_property:1,get_filename_component:1,get_property:1,get_source_file_property:1,get_target_property:1,get_test_property:1,"if":1,include:1,include_directories:2,include_external_msproject:1,include_regular_expression:2,install:1,link_directories:1,load_cache:1,load_command:1,macro:1,mark_as_advanced:1,message:1,option:1,output_required_files:1,project:1,qt_wrap_cpp:2,qt_wrap_ui:2,remove_definitions:2,"return":1,separate_arguments:1,set:1,set_directory_properties:1,set_property:1,set_source_files_properties:1,set_target_properties:1,set_tests_properties:1,site_name:1,source_group:1,string:1,target_link_libraries:2,try_compile:2,try_run:2,unset:1,variable_watch:2,"while":1,build_name:1,exec_program:1,export_library_dependencies:1,install_files:1,install_programs:1,install_targets:1,link_libraries:1,make_directory:1,remove:1,subdir_depends:1,subdirs:1,use_mangled_mesa:1,utility_source:1,variable_requires:1,write_file:1},c:[{cN:"envvar",b:"\\${",e:"}"},hljs.HCM,hljs.QSM,hljs.NM]}};hljs.LANGUAGES.objectivec=function(){var a={keyword:{"false":1,"int":1,"float":1,"while":1,"private":1,"char":1,"catch":1,"export":1,sizeof:2,typedef:2,"const":1,struct:1,"for":1,union:1,unsigned:1,"long":1,"volatile":2,"static":1,"protected":1,bool:1,mutable:1,"if":1,"public":1,"do":1,"return":1,"goto":1,"void":2,"enum":1,"else":1,"break":1,extern:1,"true":1,"class":1,asm:1,"case":1,"short":1,"default":1,"double":1,"throw":1,register:1,explicit:1,signed:1,typename:1,"try":1,"this":1,"switch":1,"continue":1,wchar_t:1,inline:1,readonly:1,assign:1,property:1,protocol:10,self:1,"synchronized":1,end:1,synthesize:50,id:1,optional:1,required:1,implementation:10,nonatomic:1,"interface":1,"super":1,unichar:1,"finally":2,dynamic:2,nil:1},built_in:{YES:5,NO:5,NULL:1,IBOutlet:50,IBAction:50,NSString:50,NSDictionary:50,CGRect:50,CGPoint:50,NSRange:50,release:1,retain:1,autorelease:50,UIButton:50,UILabel:50,UITextView:50,UIWebView:50,MKMapView:50,UISegmentedControl:50,NSObject:50,UITableViewDelegate:50,UITableViewDataSource:50,NSThread:50,UIActivityIndicator:50,UITabbar:50,UIToolBar:50,UIBarButtonItem:50,UIImageView:50,NSAutoreleasePool:50,UITableView:50,BOOL:1,NSInteger:20,CGFloat:20,NSException:50,NSLog:50,NSMutableString:50,NSMutableArray:50,NSMutableDictionary:50,NSURL:50}};return{dM:{k:a,i:""}]},{cN:"preprocessor",b:"#",e:"$"},{cN:"class",b:"interface|class|protocol|implementation",e:"({|$)",k:{"interface":1,"class":1,protocol:5,implementation:5},c:[{cN:"id",b:hljs.UIR}]}]}}}();hljs.LANGUAGES.avrasm={cI:true,dM:{k:{keyword:{adc:1,add:1,adiw:1,and:1,andi:1,asr:1,bclr:1,bld:1,brbc:1,brbs:1,brcc:1,brcs:1,"break":1,breq:1,brge:1,brhc:1,brhs:1,brid:1,brie:1,brlo:1,brlt:1,brmi:1,brne:1,brpl:1,brsh:1,brtc:1,brts:1,brvc:1,brvs:1,bset:1,bst:1,call:1,cbi:1,cbr:1,clc:1,clh:1,cli:1,cln:1,clr:1,cls:1,clt:1,clv:1,clz:1,com:1,cp:1,cpc:1,cpi:1,cpse:1,dec:1,eicall:1,eijmp:1,elpm:1,eor:1,fmul:1,fmuls:1,fmulsu:1,icall:1,ijmp:1,"in":1,inc:1,jmp:1,ld:1,ldd:1,ldi:1,lds:1,lpm:1,lsl:1,lsr:1,mov:1,movw:1,mul:1,muls:1,mulsu:1,neg:1,nop:1,or:1,ori:1,out:1,pop:1,push:1,rcall:1,ret:1,reti:1,rjmp:1,rol:1,ror:1,sbc:1,sbr:1,sbrc:1,sbrs:1,sec:1,seh:1,sbi:1,sbci:1,sbic:1,sbis:1,sbiw:1,sei:1,sen:1,ser:1,ses:1,set:1,sev:1,sez:1,sleep:1,spm:1,st:1,std:1,sts:1,sub:1,subi:1,swap:1,tst:1,wdr:1},built_in:{r0:1,r1:1,r2:1,r3:1,r4:1,r5:1,r6:1,r7:1,r8:1,r9:1,r10:1,r11:1,r12:1,r13:1,r14:1,r15:1,r16:1,r17:1,r18:1,r19:1,r20:1,r21:1,r22:1,r23:1,r24:1,r25:1,r26:1,r27:1,r28:1,r29:1,r30:1,r31:1,x:1,xh:1,xl:1,y:1,yh:1,yl:1,z:1,zh:1,zl:1,ucsr1c:1,udr1:1,ucsr1a:1,ucsr1b:1,ubrr1l:1,ubrr1h:1,ucsr0c:1,ubrr0h:1,tccr3c:1,tccr3a:1,tccr3b:1,tcnt3h:1,tcnt3l:1,ocr3ah:1,ocr3al:1,ocr3bh:1,ocr3bl:1,ocr3ch:1,ocr3cl:1,icr3h:1,icr3l:1,etimsk:1,etifr:1,tccr1c:1,ocr1ch:1,ocr1cl:1,twcr:1,twdr:1,twar:1,twsr:1,twbr:1,osccal:1,xmcra:1,xmcrb:1,eicra:1,spmcsr:1,spmcr:1,portg:1,ddrg:1,ping:1,portf:1,ddrf:1,sreg:1,sph:1,spl:1,xdiv:1,rampz:1,eicrb:1,eimsk:1,gimsk:1,gicr:1,eifr:1,gifr:1,timsk:1,tifr:1,mcucr:1,mcucsr:1,tccr0:1,tcnt0:1,ocr0:1,assr:1,tccr1a:1,tccr1b:1,tcnt1h:1,tcnt1l:1,ocr1ah:1,ocr1al:1,ocr1bh:1,ocr1bl:1,icr1h:1,icr1l:1,tccr2:1,tcnt2:1,ocr2:1,ocdr:1,wdtcr:1,sfior:1,eearh:1,eearl:1,eedr:1,eecr:1,porta:1,ddra:1,pina:1,portb:1,ddrb:1,pinb:1,portc:1,ddrc:1,pinc:1,portd:1,ddrd:1,pind:1,spdr:1,spsr:1,spcr:1,udr0:1,ucsr0a:1,ucsr0b:1,ubrr0l:1,acsr:1,admux:1,adcsr:1,adch:1,adcl:1,porte:1,ddre:1,pine:1,pinf:1}},c:[hljs.CBLCLM,{cN:"comment",b:";",e:"$"},hljs.CNM,hljs.BINARY_NUMBER_MODE,{cN:"number",b:"\\b(\\$[a-zA-Z0-9]+|0o[0-7]+)"},hljs.QSM,{cN:"string",b:"'",e:"[^\\\\]'",i:"[^\\\\][^']"},{cN:"label",b:"^[A-Za-z0-9_.$]+:"},{cN:"preprocessor",b:"#",e:"$"},{cN:"preprocessor",b:"\\.[a-zA-Z]+"},{cN:"localvars",b:"@[0-9]+"}]}};hljs.LANGUAGES.vhdl={cI:true,dM:{k:{keyword:{abs:1,access:1,after:1,alias:1,all:1,and:1,architecture:2,array:1,assert:1,attribute:1,begin:1,block:1,body:1,buffer:1,bus:1,"case":1,component:2,configuration:1,constant:1,disconnect:2,downto:2,"else":1,elsif:1,end:1,entity:2,exit:1,file:1,"for":1,"function":1,generate:2,generic:2,group:1,guarded:2,"if":0,impure:2,"in":1,inertial:1,inout:1,is:1,label:1,library:1,linkage:1,literal:1,loop:1,map:1,mod:1,nand:1,"new":1,next:1,nor:1,not:1,"null":1,of:1,on:1,open:1,or:1,others:1,out:1,"package":1,port:2,postponed:1,procedure:1,process:1,pure:2,range:1,record:1,register:1,reject:1,"return":1,rol:1,ror:1,select:1,severity:1,signal:1,shared:1,sla:1,sli:1,sra:1,srl:1,subtype:2,then:1,to:1,transport:1,type:1,units:1,until:1,use:1,variable:1,wait:1,when:1,"while":1,"with":1,xnor:1,xor:1},type:{"boolean":1,bit:1,character:1,severity_level:2,integer:1,time:1,delay_length:2,natural:1,positive:1,string:1,bit_vector:2,file_open_kind:2,file_open_status:2,std_ulogic:2,std_ulogic_vector:2,std_logic:2,std_logic_vector:2}},i:"{",c:[{cN:"comment",b:"--",e:"$"},hljs.QSM,hljs.CNM,{cN:"literal",b:"'(U|X|0|1|Z|W|L|H|-)",e:"'",c:[hljs.BE]}]}};hljs.LANGUAGES.coffeescript=function(){var d={keyword:{"in":1,"if":1,"for":1,"while":1,"finally":1,"new":1,"do":1,"return":1,"else":1,"break":1,"catch":1,"instanceof":1,"throw":1,"try":1,"this":1,"switch":1,"continue":1,"typeof":1,"delete":1,"debugger":1,"class":1,"extends":1,"super":1,then:1,unless:1,until:1,loop:2,of:2,by:1,when:2,and:1,or:1,is:1,isnt:2,not:1},literal:{"true":1,"false":1,"null":1,"undefined":1,yes:1,no:1,on:1,off:1},reserved:{"case":1,"default":1,"function":1,"var":1,"void":1,"with":1,"const":1,let:1,"enum":1,"export":1,"import":1,"native":1,__hasProp:1,__extends:1,__slice:1,__bind:1,__indexOf:1}};var a="[A-Za-z$_][0-9A-Za-z$_]*";var b={cN:"subst",b:"#\\{",e:"}",k:d,c:[hljs.CNM,hljs.BINARY_NUMBER_MODE]};var c={cN:"string",b:'"',e:'"',r:0,c:[hljs.BE,b]};var h={cN:"string",b:'"""',e:'"""',c:[hljs.BE,b]};var g={cN:"comment",b:"###",e:"###"};var f={cN:"regexp",b:"///",e:"///",c:[hljs.HCM]};var i={cN:"function",b:a+"\\s*=\\s*(\\(.+\\))?\\s*[-=]>",rB:true,c:[{cN:"title",b:a},{cN:"params",b:"\\(",e:"\\)"}]};var e={cN:"javascript",b:"`",e:"`",eB:true,eE:true,sL:"javascript"};return{dM:{k:d,c:[hljs.CNM,hljs.BINARY_NUMBER_MODE,hljs.ASM,h,c,g,hljs.HCM,f,e,i]}}}();hljs.LANGUAGES.nginx=function(){var c={cN:"variable",b:"\\$\\d+"};var b={cN:"variable",b:"\\${",e:"}"};var a={cN:"variable",b:"[\\$\\@]"+hljs.UIR};return{dM:{c:[hljs.HCM,{b:hljs.UIR,e:";|{",rE:true,k:{accept_mutex:1,accept_mutex_delay:1,access_log:1,add_after_body:1,add_before_body:1,add_header:1,addition_types:1,alias:1,allow:1,ancient_browser:1,ancient_browser_value:1,auth_basic:1,auth_basic_user_file:1,autoindex:1,autoindex_exact_size:1,autoindex_localtime:1,"break":1,charset:1,charset_map:1,charset_types:1,client_body_buffer_size:1,client_body_in_file_only:1,client_body_in_single_buffer:1,client_body_temp_path:1,client_body_timeout:1,client_header_buffer_size:1,client_header_timeout:1,client_max_body_size:1,connection_pool_size:1,connections:1,create_full_put_path:1,daemon:1,dav_access:1,dav_methods:1,debug_connection:1,debug_points:1,default_type:1,deny:1,directio:1,directio_alignment:1,echo:1,echo_after_body:1,echo_before_body:1,echo_blocking_sleep:1,echo_duplicate:1,echo_end:1,echo_exec:1,echo_flush:1,echo_foreach_split:1,echo_location:1,echo_location_async:1,echo_read_request_body:1,echo_request_body:1,echo_reset_timer:1,echo_sleep:1,echo_subrequest:1,echo_subrequest_async:1,empty_gif:1,env:1,error_log:1,error_page:1,events:1,expires:1,fastcgi_bind:1,fastcgi_buffer_size:1,fastcgi_buffers:1,fastcgi_busy_buffers_size:1,fastcgi_cache:1,fastcgi_cache_key:1,fastcgi_cache_methods:1,fastcgi_cache_min_uses:1,fastcgi_cache_path:1,fastcgi_cache_use_stale:1,fastcgi_cache_valid:1,fastcgi_catch_stderr:1,fastcgi_connect_timeout:1,fastcgi_hide_header:1,fastcgi_ignore_client_abort:1,fastcgi_ignore_headers:1,fastcgi_index:1,fastcgi_intercept_errors:1,fastcgi_max_temp_file_size:1,fastcgi_next_upstream:1,fastcgi_param:1,fastcgi_pass:1,fastcgi_pass_header:1,fastcgi_pass_request_body:1,fastcgi_pass_request_headers:1,fastcgi_read_timeout:1,fastcgi_send_lowat:1,fastcgi_send_timeout:1,fastcgi_split_path_info:1,fastcgi_store:1,fastcgi_store_access:1,fastcgi_temp_file_write_size:1,fastcgi_temp_path:1,fastcgi_upstream_fail_timeout:1,fastcgi_upstream_max_fails:1,flv:1,geo:1,geoip_city:1,geoip_country:1,gzip:1,gzip_buffers:1,gzip_comp_level:1,gzip_disable:1,gzip_hash:1,gzip_http_version:1,gzip_min_length:1,gzip_no_buffer:1,gzip_proxied:1,gzip_static:1,gzip_types:1,gzip_vary:1,gzip_window:1,http:1,"if":1,if_modified_since:1,ignore_invalid_headers:1,image_filter:1,image_filter_buffer:1,image_filter_jpeg_quality:1,image_filter_transparency:1,include:1,index:1,internal:1,ip_hash:1,js:1,js_load:1,js_require:1,js_utf8:1,keepalive_requests:1,keepalive_timeout:1,kqueue_changes:1,kqueue_events:1,large_client_header_buffers:1,limit_conn:1,limit_conn_log_level:1,limit_except:1,limit_rate:1,limit_rate_after:1,limit_req:1,limit_req_log_level:1,limit_req_zone:1,limit_zone:1,lingering_time:1,lingering_timeout:1,listen:1,location:1,lock_file:1,log_format:1,log_not_found:1,log_subrequest:1,map:1,map_hash_bucket_size:1,map_hash_max_size:1,master_process:1,memcached_bind:1,memcached_buffer_size:1,memcached_connect_timeout:1,memcached_next_upstream:1,memcached_pass:1,memcached_read_timeout:1,memcached_send_timeout:1,memcached_upstream_fail_timeout:1,memcached_upstream_max_fails:1,merge_slashes:1,min_delete_depth:1,modern_browser:1,modern_browser_value:1,more_clear_headers:1,more_clear_input_headers:1,more_set_headers:1,more_set_input_headers:1,msie_padding:1,msie_refresh:1,multi_accept:1,open_file_cache:1,open_file_cache_errors:1,open_file_cache_events:1,open_file_cache_min_uses:1,open_file_cache_retest:1,open_file_cache_valid:1,open_log_file_cache:1,optimize_server_names:1,output_buffers:1,override_charset:1,perl:1,perl_modules:1,perl_require:1,perl_set:1,pid:1,port_in_redirect:1,post_action:1,postpone_gzipping:1,postpone_output:1,proxy_bind:1,proxy_buffer_size:1,proxy_buffering:1,proxy_buffers:1,proxy_busy_buffers_size:1,proxy_cache:1,proxy_cache_key:1,proxy_cache_methods:1,proxy_cache_min_uses:1,proxy_cache_path:1,proxy_cache_use_stale:1,proxy_cache_valid:1,proxy_connect_timeout:1,proxy_headers_hash_bucket_size:1,proxy_headers_hash_max_size:1,proxy_hide_header:1,proxy_ignore_client_abort:1,proxy_ignore_headers:1,proxy_intercept_errors:1,proxy_max_temp_file_size:1,proxy_method:1,proxy_next_upstream:1,proxy_pass:1,proxy_pass_header:1,proxy_pass_request_body:1,proxy_pass_request_headers:1,proxy_read_timeout:1,proxy_redirect:1,proxy_send_lowat:1,proxy_send_timeout:1,proxy_set_body:1,proxy_set_header:1,proxy_store:1,proxy_store_access:1,proxy_temp_file_write_size:1,proxy_temp_path:1,proxy_upstream_fail_timeout:1,proxy_upstream_max_fails:1,push_authorized_channels_only:1,push_channel_group:1,push_max_channel_id_length:1,push_max_channel_subscribers:1,push_max_message_buffer_length:1,push_max_reserved_memory:1,push_message_buffer_length:1,push_message_timeout:1,push_min_message_buffer_length:1,push_min_message_recipients:1,push_publisher:1,push_store_messages:1,push_subscriber:1,push_subscriber_concurrency:1,random_index:1,read_ahead:1,real_ip_header:1,recursive_error_pages:1,request_pool_size:1,reset_timedout_connection:1,resolver:1,resolver_timeout:1,"return":1,rewrite:1,rewrite_log:1,root:1,satisfy:1,satisfy_any:1,send_lowat:1,send_timeout:1,sendfile:1,sendfile_max_chunk:1,server:1,server_name:1,server_name_in_redirect:1,server_names_hash_bucket_size:1,server_names_hash_max_size:1,server_tokens:1,set:1,set_real_ip_from:1,source_charset:1,ssi:1,ssi_ignore_recycled_buffers:1,ssi_min_file_chunk:1,ssi_silent_errors:1,ssi_types:1,ssi_value_length:1,ssl:1,ssl_certificate:1,ssl_certificate_key:1,ssl_ciphers:1,ssl_client_certificate:1,ssl_crl:1,ssl_dhparam:1,ssl_prefer_server_ciphers:1,ssl_protocols:1,ssl_session_cache:1,ssl_session_timeout:1,ssl_verify_client:1,ssl_verify_depth:1,sub_filter:1,sub_filter_once:1,sub_filter_types:1,tcp_nodelay:1,tcp_nopush:1,timer_resolution:1,try_files:1,types:1,types_hash_bucket_size:1,types_hash_max_size:1,underscores_in_headers:1,uninitialized_variable_warn:1,upstream:1,use:1,user:1,userid:1,userid_domain:1,userid_expires:1,userid_mark:1,userid_name:1,userid_p3p:1,userid_path:1,userid_service:1,valid_referers:1,variables_hash_bucket_size:1,variables_hash_max_size:1,worker_connections:1,worker_cpu_affinity:1,worker_priority:1,worker_processes:1,worker_rlimit_core:1,worker_rlimit_nofile:1,worker_rlimit_sigpending:1,working_directory:1,xml_entities:1,xslt_stylesheet:1,xslt_types:1},r:0,c:[hljs.HCM,{b:"\\s",e:"[;{]",rB:true,rE:true,l:"[a-z/]+",k:{built_in:{on:1,off:1,yes:1,no:1,"true":1,"false":1,none:1,blocked:1,debug:1,info:1,notice:1,warn:1,error:1,crit:1,select:1,permanent:1,redirect:1,kqueue:1,rtsig:1,epoll:1,poll:1,"/dev/poll":1}},r:0,c:[hljs.HCM,{cN:"string",b:'"',e:'"',c:[hljs.BE,c,b,a],r:0},{cN:"string",b:"'",e:"'",c:[hljs.BE,c,b,a],r:0},{cN:"string",b:"([a-z]+):/",e:"[;\\s]",rE:true},{cN:"regexp",b:"\\s\\^",e:"\\s|{|;",rE:true,c:[hljs.BE,c,b,a]},{cN:"regexp",b:"~\\*?\\s+",e:"\\s|{|;",rE:true,c:[hljs.BE,c,b,a]},{cN:"regexp",b:"\\*(\\.[a-z\\-]+)+",c:[hljs.BE,c,b,a]},{cN:"regexp",b:"([a-z\\-]+\\.)+\\*",c:[hljs.BE,c,b,a]},{cN:"number",b:"\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\b"},{cN:"number",b:"\\s\\d+[kKmMgGdshdwy]*\\b",r:0},c,b,a]}]}]}}}();hljs.LANGUAGES.erlang_repl={dM:{k:{special_functions:{spawn:10,spawn_link:10,self:2},reserved:{after:1,and:1,andalso:5,band:1,begin:1,bnot:1,bor:1,bsl:1,bsr:1,bxor:1,"case":1,"catch":0,cond:1,div:1,end:1,fun:0,"if":0,let:1,not:0,of:1,or:1,orelse:5,query:1,receive:0,rem:1,"try":0,when:1,xor:1}},c:[{cN:"input_number",b:"^[0-9]+> ",r:10},{cN:"comment",b:"%",e:"$"},{cN:"number",b:"\\b(\\d+#[a-fA-F0-9]+|\\d+(\\.\\d+)?([eE][-+]?\\d+)?)",r:0},hljs.ASM,hljs.QSM,{cN:"constant",b:"\\?(::)?([A-Z]\\w*(::)?)+"},{cN:"arrow",b:"->"},{cN:"ok",b:"ok"},{cN:"exclamation_mark",b:"!"},{cN:"function_or_atom",b:"(\\b[a-z'][a-zA-Z0-9_']*:[a-z'][a-zA-Z0-9_']*)|(\\b[a-z'][a-zA-Z0-9_']*)",r:0},{cN:"variable",b:"[A-Z][a-zA-Z0-9_']*",r:0}]}};hljs.LANGUAGES.django=function(){function c(f,e){return(e==undefined||(!f.cN&&e.cN=="tag")||f.cN=="value")}function d(j,e){var h={};for(var g in j){if(g!="contains"){h[g]=j[g]}var k=[];for(var f=0;j.c&&f"},hljs.QSM]}}}();hljs.LANGUAGES.cpp=function(){var a={keyword:{"false":1,"int":1,"float":1,"while":1,"private":1,"char":1,"catch":1,"export":1,virtual:1,operator:2,sizeof:2,dynamic_cast:2,typedef:2,const_cast:2,"const":1,struct:1,"for":1,static_cast:2,union:1,namespace:1,unsigned:1,"long":1,"throw":1,"volatile":2,"static":1,"protected":1,bool:1,template:1,mutable:1,"if":1,"public":1,friend:2,"do":1,"return":1,"goto":1,auto:1,"void":2,"enum":1,"else":1,"break":1,"new":1,extern:1,using:1,"true":1,"class":1,asm:1,"case":1,typeid:1,"short":1,reinterpret_cast:2,"default":1,"double":1,register:1,explicit:1,signed:1,typename:1,"try":1,"this":1,"switch":1,"continue":1,wchar_t:1,inline:1,"delete":1,alignof:1,char16_t:1,char32_t:1,constexpr:1,decltype:1,noexcept:1,nullptr:1,static_assert:1,thread_local:1,restrict:1,_Bool:1,complex:1},built_in:{std:1,string:1,cin:1,cout:1,cerr:1,clog:1,stringstream:1,istringstream:1,ostringstream:1,auto_ptr:1,deque:1,list:1,queue:1,stack:1,vector:1,map:1,set:1,bitset:1,multiset:1,multimap:1,unordered_set:1,unordered_map:1,unordered_multiset:1,unordered_multimap:1,array:1,shared_ptr:1}};return{dM:{k:a,i:"",k:a,r:10,c:["self"]}]}}}();hljs.LANGUAGES.matlab={dM:{k:{keyword:{"break":1,"case":1,"catch":1,classdef:1,"continue":1,"else":1,elseif:1,end:1,enumerated:1,events:1,"for":1,"function":1,global:1,"if":1,methods:1,otherwise:1,parfor:1,persistent:1,properties:1,"return":1,spmd:1,"switch":1,"try":1,"while":1},built_in:{sin:1,sind:1,sinh:1,asin:1,asind:1,asinh:1,cos:1,cosd:1,cosh:1,acos:1,acosd:1,acosh:1,tan:1,tand:1,tanh:1,atan:1,atand:1,atan2:1,atanh:1,sec:1,secd:1,sech:1,asec:1,asecd:1,asech:1,csc:1,cscd:1,csch:1,acsc:1,acscd:1,acsch:1,cot:1,cotd:1,coth:1,acot:1,acotd:1,acoth:1,hypot:1,exp:1,expm1:1,log:1,log1p:1,log10:1,log2:1,pow2:1,realpow:1,reallog:1,realsqrt:1,sqrt:1,nthroot:1,nextpow2:1,abs:1,angle:1,complex:1,conj:1,imag:1,real:1,unwrap:1,isreal:1,cplxpair:1,fix:1,floor:1,ceil:1,round:1,mod:1,rem:1,sign:1,airy:1,besselj:1,bessely:1,besselh:1,besseli:1,besselk:1,beta:1,betainc:1,betaln:1,ellipj:1,ellipke:1,erf:1,erfc:1,erfcx:1,erfinv:1,expint:1,gamma:1,gammainc:1,gammaln:1,psi:1,legendre:1,cross:1,dot:1,factor:1,isprime:1,primes:1,gcd:1,lcm:1,rat:1,rats:1,perms:1,nchoosek:1,factorial:1,cart2sph:1,cart2pol:1,pol2cart:1,sph2cart:1,hsv2rgb:1,rgb2hsv:1,zeros:1,ones:1,eye:1,repmat:1,rand:1,randn:1,linspace:1,logspace:1,freqspace:1,meshgrid:1,accumarray:1,size:1,length:1,ndims:1,numel:1,disp:1,isempty:1,isequal:1,isequalwithequalnans:1,cat:1,reshape:1,diag:1,blkdiag:1,tril:1,triu:1,fliplr:1,flipud:1,flipdim:1,rot90:1,find:1,end:1,sub2ind:1,ind2sub:1,bsxfun:1,ndgrid:1,permute:1,ipermute:1,shiftdim:1,circshift:1,squeeze:1,isscalar:1,isvector:1,ans:1,eps:1,realmax:1,realmin:1,pi:1,i:1,inf:1,nan:1,isnan:1,isinf:1,isfinite:1,j:1,why:1,compan:1,gallery:1,hadamard:1,hankel:1,hilb:1,invhilb:1,magic:1,pascal:1,rosser:1,toeplitz:1,vander:1,wilkinson:1},},i:'(//|"|#|/\\*|\\s+/\\w+)',c:[{cN:"function",b:"function",e:"$",k:{"function":1},c:[{cN:"title",b:hljs.UIR},{cN:"params",b:"\\(",e:"\\)"},{cN:"params",b:"\\[",e:"\\]"}]},{cN:"string",b:"'",e:"'",c:[hljs.BE,{b:"''"}]},{cN:"comment",b:"\\%",e:"$"},hljs.CNM]}};hljs.LANGUAGES.parser3={dM:{sL:"html",c:[{cN:"comment",b:"^#",e:"$"},{cN:"comment",b:"\\^rem{",e:"}",r:10,c:[{b:"{",e:"}",c:["self"]}]},{cN:"preprocessor",b:"^@(?:BASE|USE|CLASS|OPTIONS)$",r:10},{cN:"title",b:"@[\\w\\-]+\\[[\\w^;\\-]*\\](?:\\[[\\w^;\\-]*\\])?(?:.*)$"},{cN:"variable",b:"\\$\\{?[\\w\\-\\.\\:]+\\}?"},{cN:"keyword",b:"\\^[\\w\\-\\.\\:]+"},{cN:"number",b:"\\^#[0-9a-fA-F]+"},hljs.CNM]}};hljs.LANGUAGES.go=function(){var a={keyword:{"break":1,"default":1,func:1,"interface":1,select:1,"case":1,map:1,struct:1,chan:1,"else":1,"goto":1,"package":1,"switch":1,"const":1,fallthrough:1,"if":1,range:1,type:1,"continue":1,"for":1,"import":1,"return":1,"var":1,go:1,defer:1},constant:{"true":1,"false":1,iota:1,nil:1},typename:{bool:1,"byte":1,complex64:1,complex128:1,float32:1,float64:1,int8:1,int16:1,int32:1,int64:1,string:1,uint8:1,uint16:1,uint32:1,uint64:1,"int":1,uint:1,uintptr:1,rune:1},built_in:{append:1,cap:1,close:1,complex:1,copy:1,imag:1,len:1,make:1,"new":1,panic:1,print:1,println:1,real:1,recover:1,"delete":1}};return{dM:{k:a,i:" 1 ? dict[phrase] : phrase.charCodeAt(0)); - dict[phrase + currChar] = code; - code++; - phrase=currChar; - } - } - out.push(phrase.length > 1 ? dict[phrase] : phrase.charCodeAt(0)); - for (var i=0; i UTF-16 convertion - * - * Copyright (C) 1999 Masanao Izumo - * Version: 1.0 - * LastModified: Dec 25 1999 - * This library is free. You can redistribute it and/or modify it. - */ - -/* - * Interfaces: - * utf8 = utf16to8(utf16); - * utf16 = utf16to8(utf8); - */ - -function utf16to8(str) { - var out, i, len, c; - - out = ""; - len = str.length; - for(i = 0; i < len; i++) { - c = str.charCodeAt(i); - if ((c >= 0x0001) && (c <= 0x007F)) { - out += str.charAt(i); - } else if (c > 0x07FF) { - out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F)); - out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F)); - out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F)); - } else { - out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F)); - out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F)); - } - } - return out; -} - -function utf8to16(str) { - var out, i, len, c; - var char2, char3; - - out = ""; - len = str.length; - i = 0; - while(i < len) { - c = str.charCodeAt(i++); - switch(c >> 4) - { - case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: - // 0xxxxxxx - out += str.charAt(i-1); - break; - case 12: case 13: - // 110x xxxx 10xx xxxx - char2 = str.charCodeAt(i++); - out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F)); - break; - case 14: - // 1110 xxxx 10xx xxxx 10xx xxxx - char2 = str.charCodeAt(i++); - char3 = str.charCodeAt(i++); - out += String.fromCharCode(((c & 0x0F) << 12) | - ((char2 & 0x3F) << 6) | - ((char3 & 0x3F) << 0)); - break; - } - } - - return out; -} \ No newline at end of file diff --git a/views/base.tpl b/views/base.tpl index 56356d8..d112c22 100644 --- a/views/base.tpl +++ b/views/base.tpl @@ -9,6 +9,7 @@ pastebin with a burn after reading feature"> + @@ -85,7 +86,7 @@ - + 'nobr' + cline = '_printlist([' + cline + '])' + del ptrbuffer[:] # Do this before calling code() again + code(cline) + + def code(stmt): + for line in stmt.splitlines(): + codebuffer.append(' ' * len(stack) + line.strip()) + + for line in template.splitlines(True): + lineno += 1 + line = line if isinstance(line, unicode)\ + else unicode(line, encoding=self.encoding) + sline = line.lstrip() + if lineno <= 2: + m = re.search(r"%.*coding[:=]\s*([-\w\.]+)", line) + if m: self.encoding = m.group(1) + if m: line = line.replace('coding','coding (removed)') + if sline and sline[0] == '%' and sline[:2] != '%%': + line = line.split('%',1)[1].lstrip() # Full line following the % + cline = self.split_comment(line).strip() + cmd = re.split(r'[^a-zA-Z0-9_]', cline)[0] + flush() # You are actually reading this? Good luck, it's a mess :) + if cmd in self.blocks or multiline: + cmd = multiline or cmd + dedent = cmd in self.dedent_blocks # "else:" + if dedent and not oneline and not multiline: + cmd = stack.pop() + code(line) + oneline = not cline.endswith(':') # "if 1: pass" + multiline = cmd if cline.endswith('\\') else False + if not oneline and not multiline: + stack.append(cmd) + elif cmd == 'end' and stack: + code('#end(%s) %s' % (stack.pop(), line.strip()[3:])) + elif cmd == 'include': + p = cline.split(None, 2)[1:] + if len(p) == 2: + code("_=_include(%s, _stdout, %s)" % (repr(p[0]), p[1])) + elif p: + code("_=_include(%s, _stdout)" % repr(p[0])) + else: # Empty %include -> reverse of %rebase + code("_printlist(_base)") + elif cmd == 'rebase': + p = cline.split(None, 2)[1:] + if len(p) == 2: + code("globals()['_rebase']=(%s, dict(%s))" % (repr(p[0]), p[1])) + elif p: + code("globals()['_rebase']=(%s, {})" % repr(p[0])) + else: + code(line) + else: # Line starting with text (not '%') or '%%' (escaped) + if line.strip().startswith('%%'): + line = line.replace('%%', '%', 1) + ptrbuffer.append(yield_tokens(line)) + flush() + return '\n'.join(codebuffer) + '\n' + + def subtemplate(self, _name, _stdout, *args, **kwargs): + for dictarg in args: kwargs.update(dictarg) + if _name not in self.cache: + self.cache[_name] = self.__class__(name=_name, lookup=self.lookup) + return self.cache[_name].execute(_stdout, kwargs) + + def execute(self, _stdout, *args, **kwargs): + for dictarg in args: kwargs.update(dictarg) + env = self.defaults.copy() + env.update({'_stdout': _stdout, '_printlist': _stdout.extend, + '_include': self.subtemplate, '_str': self._str, + '_escape': self._escape, 'get': env.get, + 'setdefault': env.setdefault, 'defined': env.__contains__}) + env.update(kwargs) + eval(self.co, env) + if '_rebase' in env: + subtpl, rargs = env['_rebase'] + rargs['_base'] = _stdout[:] #copy stdout + del _stdout[:] # clear stdout + return self.subtemplate(subtpl,_stdout,rargs) + return env + + def render(self, *args, **kwargs): + """ Render the template using keyword arguments as local variables. """ + for dictarg in args: kwargs.update(dictarg) + stdout = [] + self.execute(stdout, kwargs) + return ''.join(stdout) + + +def template(*args, **kwargs): + ''' + Get a rendered template as a string iterator. + You can use a name, a filename or a template string as first parameter. + Template rendering arguments can be passed as dictionaries + or directly (as keyword arguments). + ''' + tpl = args[0] if args else None + template_adapter = kwargs.pop('template_adapter', SimpleTemplate) + if tpl not in TEMPLATES or DEBUG: + settings = kwargs.pop('template_settings', {}) + lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) + if isinstance(tpl, template_adapter): + TEMPLATES[tpl] = tpl + if settings: TEMPLATES[tpl].prepare(**settings) + elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl: + TEMPLATES[tpl] = template_adapter(source=tpl, lookup=lookup, **settings) + else: + TEMPLATES[tpl] = template_adapter(name=tpl, lookup=lookup, **settings) + if not TEMPLATES[tpl]: + abort(500, 'Template (%s) not found' % tpl) + for dictarg in args[1:]: kwargs.update(dictarg) + return TEMPLATES[tpl].render(kwargs) + +mako_template = functools.partial(template, template_adapter=MakoTemplate) +cheetah_template = functools.partial(template, template_adapter=CheetahTemplate) +jinja2_template = functools.partial(template, template_adapter=Jinja2Template) +simpletal_template = functools.partial(template, template_adapter=SimpleTALTemplate) + + +def view(tpl_name, **defaults): + ''' Decorator: renders a template for a handler. + The handler can control its behavior like that: + + - return a dict of template vars to fill out the template + - return something other than a dict and the view decorator will not + process the template, but return the handler result as is. + This includes returning a HTTPResponse(dict) to get, + for instance, JSON with autojson or other castfilters. + ''' + def decorator(func): + @functools.wraps(func) + def wrapper(*args, **kwargs): + result = func(*args, **kwargs) + if isinstance(result, (dict, DictMixin)): + tplvars = defaults.copy() + tplvars.update(result) + return template(tpl_name, **tplvars) + return result + return wrapper + return decorator + +mako_view = functools.partial(view, template_adapter=MakoTemplate) +cheetah_view = functools.partial(view, template_adapter=CheetahTemplate) +jinja2_view = functools.partial(view, template_adapter=Jinja2Template) +simpletal_view = functools.partial(view, template_adapter=SimpleTALTemplate) + + + + + + +############################################################################### +# Constants and Globals ######################################################## +############################################################################### + + +TEMPLATE_PATH = ['./', './views/'] +TEMPLATES = {} +DEBUG = False +NORUN = False # If set, run() does nothing. Used by load_app() + +#: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found') +HTTP_CODES = httplib.responses +HTTP_CODES[418] = "I'm a teapot" # RFC 2324 +HTTP_CODES[428] = "Precondition Required" +HTTP_CODES[429] = "Too Many Requests" +HTTP_CODES[431] = "Request Header Fields Too Large" +HTTP_CODES[511] = "Network Authentication Required" +_HTTP_STATUS_LINES = dict((k, '%d %s'%(k,v)) for (k,v) in HTTP_CODES.iteritems()) + +#: The default template used for error pages. Override with @error() +ERROR_PAGE_TEMPLATE = """ +%try: + %from bottle import DEBUG, HTTP_CODES, request, touni + %status_name = HTTP_CODES.get(e.status, 'Unknown').title() + + + + Error {{e.status}}: {{status_name}} + + + +

Error {{e.status}}: {{status_name}}

+

Sorry, the requested URL {{repr(request.url)}} + caused an error:

+
{{e.output}}
+ %if DEBUG and e.exception: +

Exception:

+
{{repr(e.exception)}}
+ %end + %if DEBUG and e.traceback: +

Traceback:

+
{{e.traceback}}
+ %end + + +%except ImportError: + ImportError: Could not generate the error page. Please add bottle to + the import path. +%end +""" + +#: A thread-safe instance of :class:`Request` representing the `current` request. +request = Request() + +#: A thread-safe instance of :class:`Response` used to build the HTTP response. +response = Response() + +#: A thread-safe namespace. Not used by Bottle. +local = threading.local() + +# Initialize app stack (create first empty Bottle app) +# BC: 0.6.4 and needed for run() +app = default_app = AppStack() +app.push() + +#: A virtual package that redirects import statements. +#: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. +ext = _ImportRedirect(__name__+'.ext', 'bottle_%s').module + +if __name__ == '__main__': + opt, args, parser = _cmd_options, _cmd_args, _cmd_parser + if opt.version: + print 'Bottle', __version__; sys.exit(0) + if not args: + parser.print_help() + print '\nError: No application specified.\n' + sys.exit(1) + + try: + sys.path.insert(0, '.') + sys.modules.setdefault('bottle', sys.modules['__main__']) + except (AttributeError, ImportError), e: + parser.error(e.args[0]) + + if opt.bind and ':' in opt.bind: + host, port = opt.bind.rsplit(':', 1) + else: + host, port = (opt.bind or 'localhost'), 8080 + + debug(opt.debug) + run(args[0], host=host, port=port, server=opt.server, reloader=opt.reload, plugins=opt.plugin) + +# THE END diff --git a/libs/cherrypy/LICENSE.txt b/libs/cherrypy/LICENSE.txt new file mode 100644 index 0000000..8db13fb --- /dev/null +++ b/libs/cherrypy/LICENSE.txt @@ -0,0 +1,25 @@ +Copyright (c) 2004-2011, CherryPy Team (team@cherrypy.org) +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of the CherryPy Team nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/libs/cherrypy/__init__.py b/libs/cherrypy/__init__.py new file mode 100644 index 0000000..41e3898 --- /dev/null +++ b/libs/cherrypy/__init__.py @@ -0,0 +1,624 @@ +"""CherryPy is a pythonic, object-oriented HTTP framework. + + +CherryPy consists of not one, but four separate API layers. + +The APPLICATION LAYER is the simplest. CherryPy applications are written as +a tree of classes and methods, where each branch in the tree corresponds to +a branch in the URL path. Each method is a 'page handler', which receives +GET and POST params as keyword arguments, and returns or yields the (HTML) +body of the response. The special method name 'index' is used for paths +that end in a slash, and the special method name 'default' is used to +handle multiple paths via a single handler. This layer also includes: + + * the 'exposed' attribute (and cherrypy.expose) + * cherrypy.quickstart() + * _cp_config attributes + * cherrypy.tools (including cherrypy.session) + * cherrypy.url() + +The ENVIRONMENT LAYER is used by developers at all levels. It provides +information about the current request and response, plus the application +and server environment, via a (default) set of top-level objects: + + * cherrypy.request + * cherrypy.response + * cherrypy.engine + * cherrypy.server + * cherrypy.tree + * cherrypy.config + * cherrypy.thread_data + * cherrypy.log + * cherrypy.HTTPError, NotFound, and HTTPRedirect + * cherrypy.lib + +The EXTENSION LAYER allows advanced users to construct and share their own +plugins. It consists of: + + * Hook API + * Tool API + * Toolbox API + * Dispatch API + * Config Namespace API + +Finally, there is the CORE LAYER, which uses the core API's to construct +the default components which are available at higher layers. You can think +of the default components as the 'reference implementation' for CherryPy. +Megaframeworks (and advanced users) may replace the default components +with customized or extended components. The core API's are: + + * Application API + * Engine API + * Request API + * Server API + * WSGI API + +These API's are described in the CherryPy specification: +http://www.cherrypy.org/wiki/CherryPySpec +""" + +__version__ = "3.2.2" + +from cherrypy._cpcompat import urljoin as _urljoin, urlencode as _urlencode +from cherrypy._cpcompat import basestring, unicodestr, set + +from cherrypy._cperror import HTTPError, HTTPRedirect, InternalRedirect +from cherrypy._cperror import NotFound, CherryPyException, TimeoutError + +from cherrypy import _cpdispatch as dispatch + +from cherrypy import _cptools +tools = _cptools.default_toolbox +Tool = _cptools.Tool + +from cherrypy import _cprequest +from cherrypy.lib import httputil as _httputil + +from cherrypy import _cptree +tree = _cptree.Tree() +from cherrypy._cptree import Application +from cherrypy import _cpwsgi as wsgi + +from cherrypy import process +try: + from cherrypy.process import win32 + engine = win32.Win32Bus() + engine.console_control_handler = win32.ConsoleCtrlHandler(engine) + del win32 +except ImportError: + engine = process.bus + + +# Timeout monitor. We add two channels to the engine +# to which cherrypy.Application will publish. +engine.listeners['before_request'] = set() +engine.listeners['after_request'] = set() + +class _TimeoutMonitor(process.plugins.Monitor): + + def __init__(self, bus): + self.servings = [] + process.plugins.Monitor.__init__(self, bus, self.run) + + def before_request(self): + self.servings.append((serving.request, serving.response)) + + def after_request(self): + try: + self.servings.remove((serving.request, serving.response)) + except ValueError: + pass + + def run(self): + """Check timeout on all responses. (Internal)""" + for req, resp in self.servings: + resp.check_timeout() +engine.timeout_monitor = _TimeoutMonitor(engine) +engine.timeout_monitor.subscribe() + +engine.autoreload = process.plugins.Autoreloader(engine) +engine.autoreload.subscribe() + +engine.thread_manager = process.plugins.ThreadManager(engine) +engine.thread_manager.subscribe() + +engine.signal_handler = process.plugins.SignalHandler(engine) + + +from cherrypy import _cpserver +server = _cpserver.Server() +server.subscribe() + + +def quickstart(root=None, script_name="", config=None): + """Mount the given root, start the builtin server (and engine), then block. + + root: an instance of a "controller class" (a collection of page handler + methods) which represents the root of the application. + script_name: a string containing the "mount point" of the application. + This should start with a slash, and be the path portion of the URL + at which to mount the given root. For example, if root.index() will + handle requests to "http://www.example.com:8080/dept/app1/", then + the script_name argument would be "/dept/app1". + + It MUST NOT end in a slash. If the script_name refers to the root + of the URI, it MUST be an empty string (not "/"). + config: a file or dict containing application config. If this contains + a [global] section, those entries will be used in the global + (site-wide) config. + """ + if config: + _global_conf_alias.update(config) + + tree.mount(root, script_name, config) + + if hasattr(engine, "signal_handler"): + engine.signal_handler.subscribe() + if hasattr(engine, "console_control_handler"): + engine.console_control_handler.subscribe() + + engine.start() + engine.block() + + +from cherrypy._cpcompat import threadlocal as _local + +class _Serving(_local): + """An interface for registering request and response objects. + + Rather than have a separate "thread local" object for the request and + the response, this class works as a single threadlocal container for + both objects (and any others which developers wish to define). In this + way, we can easily dump those objects when we stop/start a new HTTP + conversation, yet still refer to them as module-level globals in a + thread-safe way. + """ + + request = _cprequest.Request(_httputil.Host("127.0.0.1", 80), + _httputil.Host("127.0.0.1", 1111)) + """ + The request object for the current thread. In the main thread, + and any threads which are not receiving HTTP requests, this is None.""" + + response = _cprequest.Response() + """ + The response object for the current thread. In the main thread, + and any threads which are not receiving HTTP requests, this is None.""" + + def load(self, request, response): + self.request = request + self.response = response + + def clear(self): + """Remove all attributes of self.""" + self.__dict__.clear() + +serving = _Serving() + + +class _ThreadLocalProxy(object): + + __slots__ = ['__attrname__', '__dict__'] + + def __init__(self, attrname): + self.__attrname__ = attrname + + def __getattr__(self, name): + child = getattr(serving, self.__attrname__) + return getattr(child, name) + + def __setattr__(self, name, value): + if name in ("__attrname__", ): + object.__setattr__(self, name, value) + else: + child = getattr(serving, self.__attrname__) + setattr(child, name, value) + + def __delattr__(self, name): + child = getattr(serving, self.__attrname__) + delattr(child, name) + + def _get_dict(self): + child = getattr(serving, self.__attrname__) + d = child.__class__.__dict__.copy() + d.update(child.__dict__) + return d + __dict__ = property(_get_dict) + + def __getitem__(self, key): + child = getattr(serving, self.__attrname__) + return child[key] + + def __setitem__(self, key, value): + child = getattr(serving, self.__attrname__) + child[key] = value + + def __delitem__(self, key): + child = getattr(serving, self.__attrname__) + del child[key] + + def __contains__(self, key): + child = getattr(serving, self.__attrname__) + return key in child + + def __len__(self): + child = getattr(serving, self.__attrname__) + return len(child) + + def __nonzero__(self): + child = getattr(serving, self.__attrname__) + return bool(child) + # Python 3 + __bool__ = __nonzero__ + +# Create request and response object (the same objects will be used +# throughout the entire life of the webserver, but will redirect +# to the "serving" object) +request = _ThreadLocalProxy('request') +response = _ThreadLocalProxy('response') + +# Create thread_data object as a thread-specific all-purpose storage +class _ThreadData(_local): + """A container for thread-specific data.""" +thread_data = _ThreadData() + + +# Monkeypatch pydoc to allow help() to go through the threadlocal proxy. +# Jan 2007: no Googleable examples of anyone else replacing pydoc.resolve. +# The only other way would be to change what is returned from type(request) +# and that's not possible in pure Python (you'd have to fake ob_type). +def _cherrypy_pydoc_resolve(thing, forceload=0): + """Given an object or a path to an object, get the object and its name.""" + if isinstance(thing, _ThreadLocalProxy): + thing = getattr(serving, thing.__attrname__) + return _pydoc._builtin_resolve(thing, forceload) + +try: + import pydoc as _pydoc + _pydoc._builtin_resolve = _pydoc.resolve + _pydoc.resolve = _cherrypy_pydoc_resolve +except ImportError: + pass + + +from cherrypy import _cplogging + +class _GlobalLogManager(_cplogging.LogManager): + """A site-wide LogManager; routes to app.log or global log as appropriate. + + This :class:`LogManager` implements + cherrypy.log() and cherrypy.log.access(). If either + function is called during a request, the message will be sent to the + logger for the current Application. If they are called outside of a + request, the message will be sent to the site-wide logger. + """ + + def __call__(self, *args, **kwargs): + """Log the given message to the app.log or global log as appropriate.""" + # Do NOT use try/except here. See http://www.cherrypy.org/ticket/945 + if hasattr(request, 'app') and hasattr(request.app, 'log'): + log = request.app.log + else: + log = self + return log.error(*args, **kwargs) + + def access(self): + """Log an access message to the app.log or global log as appropriate.""" + try: + return request.app.log.access() + except AttributeError: + return _cplogging.LogManager.access(self) + + +log = _GlobalLogManager() +# Set a default screen handler on the global log. +log.screen = True +log.error_file = '' +# Using an access file makes CP about 10% slower. Leave off by default. +log.access_file = '' + +def _buslog(msg, level): + log.error(msg, 'ENGINE', severity=level) +engine.subscribe('log', _buslog) + +# Helper functions for CP apps # + + +def expose(func=None, alias=None): + """Expose the function, optionally providing an alias or set of aliases.""" + def expose_(func): + func.exposed = True + if alias is not None: + if isinstance(alias, basestring): + parents[alias.replace(".", "_")] = func + else: + for a in alias: + parents[a.replace(".", "_")] = func + return func + + import sys, types + if isinstance(func, (types.FunctionType, types.MethodType)): + if alias is None: + # @expose + func.exposed = True + return func + else: + # func = expose(func, alias) + parents = sys._getframe(1).f_locals + return expose_(func) + elif func is None: + if alias is None: + # @expose() + parents = sys._getframe(1).f_locals + return expose_ + else: + # @expose(alias="alias") or + # @expose(alias=["alias1", "alias2"]) + parents = sys._getframe(1).f_locals + return expose_ + else: + # @expose("alias") or + # @expose(["alias1", "alias2"]) + parents = sys._getframe(1).f_locals + alias = func + return expose_ + +def popargs(*args, **kwargs): + """A decorator for _cp_dispatch + (cherrypy.dispatch.Dispatcher.dispatch_method_name). + + Optional keyword argument: handler=(Object or Function) + + Provides a _cp_dispatch function that pops off path segments into + cherrypy.request.params under the names specified. The dispatch + is then forwarded on to the next vpath element. + + Note that any existing (and exposed) member function of the class that + popargs is applied to will override that value of the argument. For + instance, if you have a method named "list" on the class decorated with + popargs, then accessing "/list" will call that function instead of popping + it off as the requested parameter. This restriction applies to all + _cp_dispatch functions. The only way around this restriction is to create + a "blank class" whose only function is to provide _cp_dispatch. + + If there are path elements after the arguments, or more arguments + are requested than are available in the vpath, then the 'handler' + keyword argument specifies the next object to handle the parameterized + request. If handler is not specified or is None, then self is used. + If handler is a function rather than an instance, then that function + will be called with the args specified and the return value from that + function used as the next object INSTEAD of adding the parameters to + cherrypy.request.args. + + This decorator may be used in one of two ways: + + As a class decorator: + @cherrypy.popargs('year', 'month', 'day') + class Blog: + def index(self, year=None, month=None, day=None): + #Process the parameters here; any url like + #/, /2009, /2009/12, or /2009/12/31 + #will fill in the appropriate parameters. + + def create(self): + #This link will still be available at /create. Defined functions + #take precedence over arguments. + + Or as a member of a class: + class Blog: + _cp_dispatch = cherrypy.popargs('year', 'month', 'day') + #... + + The handler argument may be used to mix arguments with built in functions. + For instance, the following setup allows different activities at the + day, month, and year level: + + class DayHandler: + def index(self, year, month, day): + #Do something with this day; probably list entries + + def delete(self, year, month, day): + #Delete all entries for this day + + @cherrypy.popargs('day', handler=DayHandler()) + class MonthHandler: + def index(self, year, month): + #Do something with this month; probably list entries + + def delete(self, year, month): + #Delete all entries for this month + + @cherrypy.popargs('month', handler=MonthHandler()) + class YearHandler: + def index(self, year): + #Do something with this year + + #... + + @cherrypy.popargs('year', handler=YearHandler()) + class Root: + def index(self): + #... + + """ + + #Since keyword arg comes after *args, we have to process it ourselves + #for lower versions of python. + + handler = None + handler_call = False + for k,v in kwargs.items(): + if k == 'handler': + handler = v + else: + raise TypeError( + "cherrypy.popargs() got an unexpected keyword argument '{0}'" \ + .format(k) + ) + + import inspect + + if handler is not None \ + and (hasattr(handler, '__call__') or inspect.isclass(handler)): + handler_call = True + + def decorated(cls_or_self=None, vpath=None): + if inspect.isclass(cls_or_self): + #cherrypy.popargs is a class decorator + cls = cls_or_self + setattr(cls, dispatch.Dispatcher.dispatch_method_name, decorated) + return cls + + #We're in the actual function + self = cls_or_self + parms = {} + for arg in args: + if not vpath: + break + parms[arg] = vpath.pop(0) + + if handler is not None: + if handler_call: + return handler(**parms) + else: + request.params.update(parms) + return handler + + request.params.update(parms) + + #If we are the ultimate handler, then to prevent our _cp_dispatch + #from being called again, we will resolve remaining elements through + #getattr() directly. + if vpath: + return getattr(self, vpath.pop(0), None) + else: + return self + + return decorated + +def url(path="", qs="", script_name=None, base=None, relative=None): + """Create an absolute URL for the given path. + + If 'path' starts with a slash ('/'), this will return + (base + script_name + path + qs). + If it does not start with a slash, this returns + (base + script_name [+ request.path_info] + path + qs). + + If script_name is None, cherrypy.request will be used + to find a script_name, if available. + + If base is None, cherrypy.request.base will be used (if available). + Note that you can use cherrypy.tools.proxy to change this. + + Finally, note that this function can be used to obtain an absolute URL + for the current request path (minus the querystring) by passing no args. + If you call url(qs=cherrypy.request.query_string), you should get the + original browser URL (assuming no internal redirections). + + If relative is None or not provided, request.app.relative_urls will + be used (if available, else False). If False, the output will be an + absolute URL (including the scheme, host, vhost, and script_name). + If True, the output will instead be a URL that is relative to the + current request path, perhaps including '..' atoms. If relative is + the string 'server', the output will instead be a URL that is + relative to the server root; i.e., it will start with a slash. + """ + if isinstance(qs, (tuple, list, dict)): + qs = _urlencode(qs) + if qs: + qs = '?' + qs + + if request.app: + if not path.startswith("/"): + # Append/remove trailing slash from path_info as needed + # (this is to support mistyped URL's without redirecting; + # if you want to redirect, use tools.trailing_slash). + pi = request.path_info + if request.is_index is True: + if not pi.endswith('/'): + pi = pi + '/' + elif request.is_index is False: + if pi.endswith('/') and pi != '/': + pi = pi[:-1] + + if path == "": + path = pi + else: + path = _urljoin(pi, path) + + if script_name is None: + script_name = request.script_name + if base is None: + base = request.base + + newurl = base + script_name + path + qs + else: + # No request.app (we're being called outside a request). + # We'll have to guess the base from server.* attributes. + # This will produce very different results from the above + # if you're using vhosts or tools.proxy. + if base is None: + base = server.base() + + path = (script_name or "") + path + newurl = base + path + qs + + if './' in newurl: + # Normalize the URL by removing ./ and ../ + atoms = [] + for atom in newurl.split('/'): + if atom == '.': + pass + elif atom == '..': + atoms.pop() + else: + atoms.append(atom) + newurl = '/'.join(atoms) + + # At this point, we should have a fully-qualified absolute URL. + + if relative is None: + relative = getattr(request.app, "relative_urls", False) + + # See http://www.ietf.org/rfc/rfc2396.txt + if relative == 'server': + # "A relative reference beginning with a single slash character is + # termed an absolute-path reference, as defined by ..." + # This is also sometimes called "server-relative". + newurl = '/' + '/'.join(newurl.split('/', 3)[3:]) + elif relative: + # "A relative reference that does not begin with a scheme name + # or a slash character is termed a relative-path reference." + old = url(relative=False).split('/')[:-1] + new = newurl.split('/') + while old and new: + a, b = old[0], new[0] + if a != b: + break + old.pop(0) + new.pop(0) + new = (['..'] * len(old)) + new + newurl = '/'.join(new) + + return newurl + + +# import _cpconfig last so it can reference other top-level objects +from cherrypy import _cpconfig +# Use _global_conf_alias so quickstart can use 'config' as an arg +# without shadowing cherrypy.config. +config = _global_conf_alias = _cpconfig.Config() +config.defaults = { + 'tools.log_tracebacks.on': True, + 'tools.log_headers.on': True, + 'tools.trailing_slash.on': True, + 'tools.encode.on': True + } +config.namespaces["log"] = lambda k, v: setattr(log, k, v) +config.namespaces["checker"] = lambda k, v: setattr(checker, k, v) +# Must reset to get our defaults applied. +config.reset() + +from cherrypy import _cpchecker +checker = _cpchecker.Checker() +engine.subscribe('start', checker) diff --git a/libs/cherrypy/_cpchecker.py b/libs/cherrypy/_cpchecker.py new file mode 100644 index 0000000..7ccfd89 --- /dev/null +++ b/libs/cherrypy/_cpchecker.py @@ -0,0 +1,327 @@ +import os +import warnings + +import cherrypy +from cherrypy._cpcompat import iteritems, copykeys, builtins + + +class Checker(object): + """A checker for CherryPy sites and their mounted applications. + + When this object is called at engine startup, it executes each + of its own methods whose names start with ``check_``. If you wish + to disable selected checks, simply add a line in your global + config which sets the appropriate method to False:: + + [global] + checker.check_skipped_app_config = False + + You may also dynamically add or replace ``check_*`` methods in this way. + """ + + on = True + """If True (the default), run all checks; if False, turn off all checks.""" + + + def __init__(self): + self._populate_known_types() + + def __call__(self): + """Run all check_* methods.""" + if self.on: + oldformatwarning = warnings.formatwarning + warnings.formatwarning = self.formatwarning + try: + for name in dir(self): + if name.startswith("check_"): + method = getattr(self, name) + if method and hasattr(method, '__call__'): + method() + finally: + warnings.formatwarning = oldformatwarning + + def formatwarning(self, message, category, filename, lineno, line=None): + """Function to format a warning.""" + return "CherryPy Checker:\n%s\n\n" % message + + # This value should be set inside _cpconfig. + global_config_contained_paths = False + + def check_app_config_entries_dont_start_with_script_name(self): + """Check for Application config with sections that repeat script_name.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + if not app.config: + continue + if sn == '': + continue + sn_atoms = sn.strip("/").split("/") + for key in app.config.keys(): + key_atoms = key.strip("/").split("/") + if key_atoms[:len(sn_atoms)] == sn_atoms: + warnings.warn( + "The application mounted at %r has config " \ + "entries that start with its script name: %r" % (sn, key)) + + def check_site_config_entries_in_app_config(self): + """Check for mounted Applications that have site-scoped config.""" + for sn, app in iteritems(cherrypy.tree.apps): + if not isinstance(app, cherrypy.Application): + continue + + msg = [] + for section, entries in iteritems(app.config): + if section.startswith('/'): + for key, value in iteritems(entries): + for n in ("engine.", "server.", "tree.", "checker."): + if key.startswith(n): + msg.append("[%s] %s = %s" % (section, key, value)) + if msg: + msg.insert(0, + "The application mounted at %r contains the following " + "config entries, which are only allowed in site-wide " + "config. Move them to a [global] section and pass them " + "to cherrypy.config.update() instead of tree.mount()." % sn) + warnings.warn(os.linesep.join(msg)) + + def check_skipped_app_config(self): + """Check for mounted Applications that have no config.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + if not app.config: + msg = "The Application mounted at %r has an empty config." % sn + if self.global_config_contained_paths: + msg += (" It looks like the config you passed to " + "cherrypy.config.update() contains application-" + "specific sections. You must explicitly pass " + "application config via " + "cherrypy.tree.mount(..., config=app_config)") + warnings.warn(msg) + return + + def check_app_config_brackets(self): + """Check for Application config with extraneous brackets in section names.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + if not app.config: + continue + for key in app.config.keys(): + if key.startswith("[") or key.endswith("]"): + warnings.warn( + "The application mounted at %r has config " \ + "section names with extraneous brackets: %r. " + "Config *files* need brackets; config *dicts* " + "(e.g. passed to tree.mount) do not." % (sn, key)) + + def check_static_paths(self): + """Check Application config for incorrect static paths.""" + # Use the dummy Request object in the main thread. + request = cherrypy.request + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + request.app = app + for section in app.config: + # get_resource will populate request.config + request.get_resource(section + "/dummy.html") + conf = request.config.get + + if conf("tools.staticdir.on", False): + msg = "" + root = conf("tools.staticdir.root") + dir = conf("tools.staticdir.dir") + if dir is None: + msg = "tools.staticdir.dir is not set." + else: + fulldir = "" + if os.path.isabs(dir): + fulldir = dir + if root: + msg = ("dir is an absolute path, even " + "though a root is provided.") + testdir = os.path.join(root, dir[1:]) + if os.path.exists(testdir): + msg += ("\nIf you meant to serve the " + "filesystem folder at %r, remove " + "the leading slash from dir." % testdir) + else: + if not root: + msg = "dir is a relative path and no root provided." + else: + fulldir = os.path.join(root, dir) + if not os.path.isabs(fulldir): + msg = "%r is not an absolute path." % fulldir + + if fulldir and not os.path.exists(fulldir): + if msg: + msg += "\n" + msg += ("%r (root + dir) is not an existing " + "filesystem path." % fulldir) + + if msg: + warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r" + % (msg, section, root, dir)) + + + # -------------------------- Compatibility -------------------------- # + + obsolete = { + 'server.default_content_type': 'tools.response_headers.headers', + 'log_access_file': 'log.access_file', + 'log_config_options': None, + 'log_file': 'log.error_file', + 'log_file_not_found': None, + 'log_request_headers': 'tools.log_headers.on', + 'log_to_screen': 'log.screen', + 'show_tracebacks': 'request.show_tracebacks', + 'throw_errors': 'request.throw_errors', + 'profiler.on': ('cherrypy.tree.mount(profiler.make_app(' + 'cherrypy.Application(Root())))'), + } + + deprecated = {} + + def _compat(self, config): + """Process config and warn on each obsolete or deprecated entry.""" + for section, conf in config.items(): + if isinstance(conf, dict): + for k, v in conf.items(): + if k in self.obsolete: + warnings.warn("%r is obsolete. Use %r instead.\n" + "section: [%s]" % + (k, self.obsolete[k], section)) + elif k in self.deprecated: + warnings.warn("%r is deprecated. Use %r instead.\n" + "section: [%s]" % + (k, self.deprecated[k], section)) + else: + if section in self.obsolete: + warnings.warn("%r is obsolete. Use %r instead." + % (section, self.obsolete[section])) + elif section in self.deprecated: + warnings.warn("%r is deprecated. Use %r instead." + % (section, self.deprecated[section])) + + def check_compatibility(self): + """Process config and warn on each obsolete or deprecated entry.""" + self._compat(cherrypy.config) + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + self._compat(app.config) + + + # ------------------------ Known Namespaces ------------------------ # + + extra_config_namespaces = [] + + def _known_ns(self, app): + ns = ["wsgi"] + ns.extend(copykeys(app.toolboxes)) + ns.extend(copykeys(app.namespaces)) + ns.extend(copykeys(app.request_class.namespaces)) + ns.extend(copykeys(cherrypy.config.namespaces)) + ns += self.extra_config_namespaces + + for section, conf in app.config.items(): + is_path_section = section.startswith("/") + if is_path_section and isinstance(conf, dict): + for k, v in conf.items(): + atoms = k.split(".") + if len(atoms) > 1: + if atoms[0] not in ns: + # Spit out a special warning if a known + # namespace is preceded by "cherrypy." + if (atoms[0] == "cherrypy" and atoms[1] in ns): + msg = ("The config entry %r is invalid; " + "try %r instead.\nsection: [%s]" + % (k, ".".join(atoms[1:]), section)) + else: + msg = ("The config entry %r is invalid, because " + "the %r config namespace is unknown.\n" + "section: [%s]" % (k, atoms[0], section)) + warnings.warn(msg) + elif atoms[0] == "tools": + if atoms[1] not in dir(cherrypy.tools): + msg = ("The config entry %r may be invalid, " + "because the %r tool was not found.\n" + "section: [%s]" % (k, atoms[1], section)) + warnings.warn(msg) + + def check_config_namespaces(self): + """Process config and warn on each unknown config namespace.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + self._known_ns(app) + + + + + # -------------------------- Config Types -------------------------- # + + known_config_types = {} + + def _populate_known_types(self): + b = [x for x in vars(builtins).values() + if type(x) is type(str)] + + def traverse(obj, namespace): + for name in dir(obj): + # Hack for 3.2's warning about body_params + if name == 'body_params': + continue + vtype = type(getattr(obj, name, None)) + if vtype in b: + self.known_config_types[namespace + "." + name] = vtype + + traverse(cherrypy.request, "request") + traverse(cherrypy.response, "response") + traverse(cherrypy.server, "server") + traverse(cherrypy.engine, "engine") + traverse(cherrypy.log, "log") + + def _known_types(self, config): + msg = ("The config entry %r in section %r is of type %r, " + "which does not match the expected type %r.") + + for section, conf in config.items(): + if isinstance(conf, dict): + for k, v in conf.items(): + if v is not None: + expected_type = self.known_config_types.get(k, None) + vtype = type(v) + if expected_type and vtype != expected_type: + warnings.warn(msg % (k, section, vtype.__name__, + expected_type.__name__)) + else: + k, v = section, conf + if v is not None: + expected_type = self.known_config_types.get(k, None) + vtype = type(v) + if expected_type and vtype != expected_type: + warnings.warn(msg % (k, section, vtype.__name__, + expected_type.__name__)) + + def check_config_types(self): + """Assert that config values are of the same type as default values.""" + self._known_types(cherrypy.config) + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + self._known_types(app.config) + + + # -------------------- Specific config warnings -------------------- # + + def check_localhost(self): + """Warn if any socket_host is 'localhost'. See #711.""" + for k, v in cherrypy.config.items(): + if k == 'server.socket_host' and v == 'localhost': + warnings.warn("The use of 'localhost' as a socket host can " + "cause problems on newer systems, since 'localhost' can " + "map to either an IPv4 or an IPv6 address. You should " + "use '127.0.0.1' or '[::1]' instead.") diff --git a/libs/cherrypy/_cpcompat.py b/libs/cherrypy/_cpcompat.py new file mode 100644 index 0000000..ed24c1a --- /dev/null +++ b/libs/cherrypy/_cpcompat.py @@ -0,0 +1,318 @@ +"""Compatibility code for using CherryPy with various versions of Python. + +CherryPy 3.2 is compatible with Python versions 2.3+. This module provides a +useful abstraction over the differences between Python versions, sometimes by +preferring a newer idiom, sometimes an older one, and sometimes a custom one. + +In particular, Python 2 uses str and '' for byte strings, while Python 3 +uses str and '' for unicode strings. We will call each of these the 'native +string' type for each version. Because of this major difference, this module +provides new 'bytestr', 'unicodestr', and 'nativestr' attributes, as well as +two functions: 'ntob', which translates native strings (of type 'str') into +byte strings regardless of Python version, and 'ntou', which translates native +strings to unicode strings. This also provides a 'BytesIO' name for dealing +specifically with bytes, and a 'StringIO' name for dealing with native strings. +It also provides a 'base64_decode' function with native strings as input and +output. +""" +import os +import re +import sys + +if sys.version_info >= (3, 0): + py3k = True + bytestr = bytes + unicodestr = str + nativestr = unicodestr + basestring = (bytes, str) + def ntob(n, encoding='ISO-8859-1'): + """Return the given native string as a byte string in the given encoding.""" + # In Python 3, the native string type is unicode + return n.encode(encoding) + def ntou(n, encoding='ISO-8859-1'): + """Return the given native string as a unicode string with the given encoding.""" + # In Python 3, the native string type is unicode + return n + def tonative(n, encoding='ISO-8859-1'): + """Return the given string as a native string in the given encoding.""" + # In Python 3, the native string type is unicode + if isinstance(n, bytes): + return n.decode(encoding) + return n + # type("") + from io import StringIO + # bytes: + from io import BytesIO as BytesIO +else: + # Python 2 + py3k = False + bytestr = str + unicodestr = unicode + nativestr = bytestr + basestring = basestring + def ntob(n, encoding='ISO-8859-1'): + """Return the given native string as a byte string in the given encoding.""" + # In Python 2, the native string type is bytes. Assume it's already + # in the given encoding, which for ISO-8859-1 is almost always what + # was intended. + return n + def ntou(n, encoding='ISO-8859-1'): + """Return the given native string as a unicode string with the given encoding.""" + # In Python 2, the native string type is bytes. + # First, check for the special encoding 'escape'. The test suite uses this + # to signal that it wants to pass a string with embedded \uXXXX escapes, + # but without having to prefix it with u'' for Python 2, but no prefix + # for Python 3. + if encoding == 'escape': + return unicode( + re.sub(r'\\u([0-9a-zA-Z]{4})', + lambda m: unichr(int(m.group(1), 16)), + n.decode('ISO-8859-1'))) + # Assume it's already in the given encoding, which for ISO-8859-1 is almost + # always what was intended. + return n.decode(encoding) + def tonative(n, encoding='ISO-8859-1'): + """Return the given string as a native string in the given encoding.""" + # In Python 2, the native string type is bytes. + if isinstance(n, unicode): + return n.encode(encoding) + return n + try: + # type("") + from cStringIO import StringIO + except ImportError: + # type("") + from StringIO import StringIO + # bytes: + BytesIO = StringIO + +try: + set = set +except NameError: + from sets import Set as set + +try: + # Python 3.1+ + from base64 import decodebytes as _base64_decodebytes +except ImportError: + # Python 3.0- + # since CherryPy claims compability with Python 2.3, we must use + # the legacy API of base64 + from base64 import decodestring as _base64_decodebytes + +def base64_decode(n, encoding='ISO-8859-1'): + """Return the native string base64-decoded (as a native string).""" + if isinstance(n, unicodestr): + b = n.encode(encoding) + else: + b = n + b = _base64_decodebytes(b) + if nativestr is unicodestr: + return b.decode(encoding) + else: + return b + +try: + # Python 2.5+ + from hashlib import md5 +except ImportError: + from md5 import new as md5 + +try: + # Python 2.5+ + from hashlib import sha1 as sha +except ImportError: + from sha import new as sha + +try: + sorted = sorted +except NameError: + def sorted(i): + i = i[:] + i.sort() + return i + +try: + reversed = reversed +except NameError: + def reversed(x): + i = len(x) + while i > 0: + i -= 1 + yield x[i] + +try: + # Python 3 + from urllib.parse import urljoin, urlencode + from urllib.parse import quote, quote_plus + from urllib.request import unquote, urlopen + from urllib.request import parse_http_list, parse_keqv_list +except ImportError: + # Python 2 + from urlparse import urljoin + from urllib import urlencode, urlopen + from urllib import quote, quote_plus + from urllib import unquote + from urllib2 import parse_http_list, parse_keqv_list + +try: + from threading import local as threadlocal +except ImportError: + from cherrypy._cpthreadinglocal import local as threadlocal + +try: + dict.iteritems + # Python 2 + iteritems = lambda d: d.iteritems() + copyitems = lambda d: d.items() +except AttributeError: + # Python 3 + iteritems = lambda d: d.items() + copyitems = lambda d: list(d.items()) + +try: + dict.iterkeys + # Python 2 + iterkeys = lambda d: d.iterkeys() + copykeys = lambda d: d.keys() +except AttributeError: + # Python 3 + iterkeys = lambda d: d.keys() + copykeys = lambda d: list(d.keys()) + +try: + dict.itervalues + # Python 2 + itervalues = lambda d: d.itervalues() + copyvalues = lambda d: d.values() +except AttributeError: + # Python 3 + itervalues = lambda d: d.values() + copyvalues = lambda d: list(d.values()) + +try: + # Python 3 + import builtins +except ImportError: + # Python 2 + import __builtin__ as builtins + +try: + # Python 2. We have to do it in this order so Python 2 builds + # don't try to import the 'http' module from cherrypy.lib + from Cookie import SimpleCookie, CookieError + from httplib import BadStatusLine, HTTPConnection, HTTPSConnection, IncompleteRead, NotConnected + from BaseHTTPServer import BaseHTTPRequestHandler +except ImportError: + # Python 3 + from http.cookies import SimpleCookie, CookieError + from http.client import BadStatusLine, HTTPConnection, HTTPSConnection, IncompleteRead, NotConnected + from http.server import BaseHTTPRequestHandler + +try: + # Python 2. We have to do it in this order so Python 2 builds + # don't try to import the 'http' module from cherrypy.lib + from httplib import HTTPSConnection +except ImportError: + try: + # Python 3 + from http.client import HTTPSConnection + except ImportError: + # Some platforms which don't have SSL don't expose HTTPSConnection + HTTPSConnection = None + +try: + # Python 2 + xrange = xrange +except NameError: + # Python 3 + xrange = range + +import threading +if hasattr(threading.Thread, "daemon"): + # Python 2.6+ + def get_daemon(t): + return t.daemon + def set_daemon(t, val): + t.daemon = val +else: + def get_daemon(t): + return t.isDaemon() + def set_daemon(t, val): + t.setDaemon(val) + +try: + from email.utils import formatdate + def HTTPDate(timeval=None): + return formatdate(timeval, usegmt=True) +except ImportError: + from rfc822 import formatdate as HTTPDate + +try: + # Python 3 + from urllib.parse import unquote as parse_unquote + def unquote_qs(atom, encoding, errors='strict'): + return parse_unquote(atom.replace('+', ' '), encoding=encoding, errors=errors) +except ImportError: + # Python 2 + from urllib import unquote as parse_unquote + def unquote_qs(atom, encoding, errors='strict'): + return parse_unquote(atom.replace('+', ' ')).decode(encoding, errors) + +try: + # Prefer simplejson, which is usually more advanced than the builtin module. + import simplejson as json + json_decode = json.JSONDecoder().decode + json_encode = json.JSONEncoder().iterencode +except ImportError: + if py3k: + # Python 3.0: json is part of the standard library, + # but outputs unicode. We need bytes. + import json + json_decode = json.JSONDecoder().decode + _json_encode = json.JSONEncoder().iterencode + def json_encode(value): + for chunk in _json_encode(value): + yield chunk.encode('utf8') + elif sys.version_info >= (2, 6): + # Python 2.6: json is part of the standard library + import json + json_decode = json.JSONDecoder().decode + json_encode = json.JSONEncoder().iterencode + else: + json = None + def json_decode(s): + raise ValueError('No JSON library is available') + def json_encode(s): + raise ValueError('No JSON library is available') + +try: + import cPickle as pickle +except ImportError: + # In Python 2, pickle is a Python version. + # In Python 3, pickle is the sped-up C version. + import pickle + +try: + os.urandom(20) + import binascii + def random20(): + return binascii.hexlify(os.urandom(20)).decode('ascii') +except (AttributeError, NotImplementedError): + import random + # os.urandom not available until Python 2.4. Fall back to random.random. + def random20(): + return sha('%s' % random.random()).hexdigest() + +try: + from _thread import get_ident as get_thread_ident +except ImportError: + from thread import get_ident as get_thread_ident + +try: + # Python 3 + next = next +except NameError: + # Python 2 + def next(i): + return i.next() diff --git a/libs/cherrypy/_cpconfig.py b/libs/cherrypy/_cpconfig.py new file mode 100644 index 0000000..7b4c6a4 --- /dev/null +++ b/libs/cherrypy/_cpconfig.py @@ -0,0 +1,295 @@ +""" +Configuration system for CherryPy. + +Configuration in CherryPy is implemented via dictionaries. Keys are strings +which name the mapped value, which may be of any type. + + +Architecture +------------ + +CherryPy Requests are part of an Application, which runs in a global context, +and configuration data may apply to any of those three scopes: + +Global + Configuration entries which apply everywhere are stored in + cherrypy.config. + +Application + Entries which apply to each mounted application are stored + on the Application object itself, as 'app.config'. This is a two-level + dict where each key is a path, or "relative URL" (for example, "/" or + "/path/to/my/page"), and each value is a config dict. Usually, this + data is provided in the call to tree.mount(root(), config=conf), + although you may also use app.merge(conf). + +Request + Each Request object possesses a single 'Request.config' dict. + Early in the request process, this dict is populated by merging global + config entries, Application entries (whose path equals or is a parent + of Request.path_info), and any config acquired while looking up the + page handler (see next). + + +Declaration +----------- + +Configuration data may be supplied as a Python dictionary, as a filename, +or as an open file object. When you supply a filename or file, CherryPy +uses Python's builtin ConfigParser; you declare Application config by +writing each path as a section header:: + + [/path/to/my/page] + request.stream = True + +To declare global configuration entries, place them in a [global] section. + +You may also declare config entries directly on the classes and methods +(page handlers) that make up your CherryPy application via the ``_cp_config`` +attribute. For example:: + + class Demo: + _cp_config = {'tools.gzip.on': True} + + def index(self): + return "Hello world" + index.exposed = True + index._cp_config = {'request.show_tracebacks': False} + +.. note:: + + This behavior is only guaranteed for the default dispatcher. + Other dispatchers may have different restrictions on where + you can attach _cp_config attributes. + + +Namespaces +---------- + +Configuration keys are separated into namespaces by the first "." in the key. +Current namespaces: + +engine + Controls the 'application engine', including autoreload. + These can only be declared in the global config. + +tree + Grafts cherrypy.Application objects onto cherrypy.tree. + These can only be declared in the global config. + +hooks + Declares additional request-processing functions. + +log + Configures the logging for each application. + These can only be declared in the global or / config. + +request + Adds attributes to each Request. + +response + Adds attributes to each Response. + +server + Controls the default HTTP server via cherrypy.server. + These can only be declared in the global config. + +tools + Runs and configures additional request-processing packages. + +wsgi + Adds WSGI middleware to an Application's "pipeline". + These can only be declared in the app's root config ("/"). + +checker + Controls the 'checker', which looks for common errors in + app state (including config) when the engine starts. + Global config only. + +The only key that does not exist in a namespace is the "environment" entry. +This special entry 'imports' other config entries from a template stored in +cherrypy._cpconfig.environments[environment]. It only applies to the global +config, and only when you use cherrypy.config.update. + +You can define your own namespaces to be called at the Global, Application, +or Request level, by adding a named handler to cherrypy.config.namespaces, +app.namespaces, or app.request_class.namespaces. The name can +be any string, and the handler must be either a callable or a (Python 2.5 +style) context manager. +""" + +import cherrypy +from cherrypy._cpcompat import set, basestring +from cherrypy.lib import reprconf + +# Deprecated in CherryPy 3.2--remove in 3.3 +NamespaceSet = reprconf.NamespaceSet + +def merge(base, other): + """Merge one app config (from a dict, file, or filename) into another. + + If the given config is a filename, it will be appended to + the list of files to monitor for "autoreload" changes. + """ + if isinstance(other, basestring): + cherrypy.engine.autoreload.files.add(other) + + # Load other into base + for section, value_map in reprconf.as_dict(other).items(): + if not isinstance(value_map, dict): + raise ValueError( + "Application config must include section headers, but the " + "config you tried to merge doesn't have any sections. " + "Wrap your config in another dict with paths as section " + "headers, for example: {'/': config}.") + base.setdefault(section, {}).update(value_map) + + +class Config(reprconf.Config): + """The 'global' configuration data for the entire CherryPy process.""" + + def update(self, config): + """Update self from a dict, file or filename.""" + if isinstance(config, basestring): + # Filename + cherrypy.engine.autoreload.files.add(config) + reprconf.Config.update(self, config) + + def _apply(self, config): + """Update self from a dict.""" + if isinstance(config.get("global", None), dict): + if len(config) > 1: + cherrypy.checker.global_config_contained_paths = True + config = config["global"] + if 'tools.staticdir.dir' in config: + config['tools.staticdir.section'] = "global" + reprconf.Config._apply(self, config) + + def __call__(self, *args, **kwargs): + """Decorator for page handlers to set _cp_config.""" + if args: + raise TypeError( + "The cherrypy.config decorator does not accept positional " + "arguments; you must use keyword arguments.") + def tool_decorator(f): + if not hasattr(f, "_cp_config"): + f._cp_config = {} + for k, v in kwargs.items(): + f._cp_config[k] = v + return f + return tool_decorator + + +Config.environments = environments = { + "staging": { + 'engine.autoreload_on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': False, + 'request.show_mismatched_params': False, + }, + "production": { + 'engine.autoreload_on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': False, + 'request.show_mismatched_params': False, + 'log.screen': False, + }, + "embedded": { + # For use with CherryPy embedded in another deployment stack. + 'engine.autoreload_on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': False, + 'request.show_mismatched_params': False, + 'log.screen': False, + 'engine.SIGHUP': None, + 'engine.SIGTERM': None, + }, + "test_suite": { + 'engine.autoreload_on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': True, + 'request.show_mismatched_params': True, + 'log.screen': False, + }, + } + + +def _server_namespace_handler(k, v): + """Config handler for the "server" namespace.""" + atoms = k.split(".", 1) + if len(atoms) > 1: + # Special-case config keys of the form 'server.servername.socket_port' + # to configure additional HTTP servers. + if not hasattr(cherrypy, "servers"): + cherrypy.servers = {} + + servername, k = atoms + if servername not in cherrypy.servers: + from cherrypy import _cpserver + cherrypy.servers[servername] = _cpserver.Server() + # On by default, but 'on = False' can unsubscribe it (see below). + cherrypy.servers[servername].subscribe() + + if k == 'on': + if v: + cherrypy.servers[servername].subscribe() + else: + cherrypy.servers[servername].unsubscribe() + else: + setattr(cherrypy.servers[servername], k, v) + else: + setattr(cherrypy.server, k, v) +Config.namespaces["server"] = _server_namespace_handler + +def _engine_namespace_handler(k, v): + """Backward compatibility handler for the "engine" namespace.""" + engine = cherrypy.engine + if k == 'autoreload_on': + if v: + engine.autoreload.subscribe() + else: + engine.autoreload.unsubscribe() + elif k == 'autoreload_frequency': + engine.autoreload.frequency = v + elif k == 'autoreload_match': + engine.autoreload.match = v + elif k == 'reload_files': + engine.autoreload.files = set(v) + elif k == 'deadlock_poll_freq': + engine.timeout_monitor.frequency = v + elif k == 'SIGHUP': + engine.listeners['SIGHUP'] = set([v]) + elif k == 'SIGTERM': + engine.listeners['SIGTERM'] = set([v]) + elif "." in k: + plugin, attrname = k.split(".", 1) + plugin = getattr(engine, plugin) + if attrname == 'on': + if v and hasattr(getattr(plugin, 'subscribe', None), '__call__'): + plugin.subscribe() + return + elif (not v) and hasattr(getattr(plugin, 'unsubscribe', None), '__call__'): + plugin.unsubscribe() + return + setattr(plugin, attrname, v) + else: + setattr(engine, k, v) +Config.namespaces["engine"] = _engine_namespace_handler + + +def _tree_namespace_handler(k, v): + """Namespace handler for the 'tree' config namespace.""" + if isinstance(v, dict): + for script_name, app in v.items(): + cherrypy.tree.graft(app, script_name) + cherrypy.engine.log("Mounted: %s on %s" % (app, script_name or "/")) + else: + cherrypy.tree.graft(v, v.script_name) + cherrypy.engine.log("Mounted: %s on %s" % (v, v.script_name or "/")) +Config.namespaces["tree"] = _tree_namespace_handler + + diff --git a/libs/cherrypy/_cpdispatch.py b/libs/cherrypy/_cpdispatch.py new file mode 100644 index 0000000..d614e08 --- /dev/null +++ b/libs/cherrypy/_cpdispatch.py @@ -0,0 +1,636 @@ +"""CherryPy dispatchers. + +A 'dispatcher' is the object which looks up the 'page handler' callable +and collects config for the current request based on the path_info, other +request attributes, and the application architecture. The core calls the +dispatcher as early as possible, passing it a 'path_info' argument. + +The default dispatcher discovers the page handler by matching path_info +to a hierarchical arrangement of objects, starting at request.app.root. +""" + +import string +import sys +import types +try: + classtype = (type, types.ClassType) +except AttributeError: + classtype = type + +import cherrypy +from cherrypy._cpcompat import set + + +class PageHandler(object): + """Callable which sets response.body.""" + + def __init__(self, callable, *args, **kwargs): + self.callable = callable + self.args = args + self.kwargs = kwargs + + def __call__(self): + try: + return self.callable(*self.args, **self.kwargs) + except TypeError: + x = sys.exc_info()[1] + try: + test_callable_spec(self.callable, self.args, self.kwargs) + except cherrypy.HTTPError: + raise sys.exc_info()[1] + except: + raise x + raise + + +def test_callable_spec(callable, callable_args, callable_kwargs): + """ + Inspect callable and test to see if the given args are suitable for it. + + When an error occurs during the handler's invoking stage there are 2 + erroneous cases: + 1. Too many parameters passed to a function which doesn't define + one of *args or **kwargs. + 2. Too little parameters are passed to the function. + + There are 3 sources of parameters to a cherrypy handler. + 1. query string parameters are passed as keyword parameters to the handler. + 2. body parameters are also passed as keyword parameters. + 3. when partial matching occurs, the final path atoms are passed as + positional args. + Both the query string and path atoms are part of the URI. If they are + incorrect, then a 404 Not Found should be raised. Conversely the body + parameters are part of the request; if they are invalid a 400 Bad Request. + """ + show_mismatched_params = getattr( + cherrypy.serving.request, 'show_mismatched_params', False) + try: + (args, varargs, varkw, defaults) = inspect.getargspec(callable) + except TypeError: + if isinstance(callable, object) and hasattr(callable, '__call__'): + (args, varargs, varkw, defaults) = inspect.getargspec(callable.__call__) + else: + # If it wasn't one of our own types, re-raise + # the original error + raise + + if args and args[0] == 'self': + args = args[1:] + + arg_usage = dict([(arg, 0,) for arg in args]) + vararg_usage = 0 + varkw_usage = 0 + extra_kwargs = set() + + for i, value in enumerate(callable_args): + try: + arg_usage[args[i]] += 1 + except IndexError: + vararg_usage += 1 + + for key in callable_kwargs.keys(): + try: + arg_usage[key] += 1 + except KeyError: + varkw_usage += 1 + extra_kwargs.add(key) + + # figure out which args have defaults. + args_with_defaults = args[-len(defaults or []):] + for i, val in enumerate(defaults or []): + # Defaults take effect only when the arg hasn't been used yet. + if arg_usage[args_with_defaults[i]] == 0: + arg_usage[args_with_defaults[i]] += 1 + + missing_args = [] + multiple_args = [] + for key, usage in arg_usage.items(): + if usage == 0: + missing_args.append(key) + elif usage > 1: + multiple_args.append(key) + + if missing_args: + # In the case where the method allows body arguments + # there are 3 potential errors: + # 1. not enough query string parameters -> 404 + # 2. not enough body parameters -> 400 + # 3. not enough path parts (partial matches) -> 404 + # + # We can't actually tell which case it is, + # so I'm raising a 404 because that covers 2/3 of the + # possibilities + # + # In the case where the method does not allow body + # arguments it's definitely a 404. + message = None + if show_mismatched_params: + message="Missing parameters: %s" % ",".join(missing_args) + raise cherrypy.HTTPError(404, message=message) + + # the extra positional arguments come from the path - 404 Not Found + if not varargs and vararg_usage > 0: + raise cherrypy.HTTPError(404) + + body_params = cherrypy.serving.request.body.params or {} + body_params = set(body_params.keys()) + qs_params = set(callable_kwargs.keys()) - body_params + + if multiple_args: + if qs_params.intersection(set(multiple_args)): + # If any of the multiple parameters came from the query string then + # it's a 404 Not Found + error = 404 + else: + # Otherwise it's a 400 Bad Request + error = 400 + + message = None + if show_mismatched_params: + message="Multiple values for parameters: "\ + "%s" % ",".join(multiple_args) + raise cherrypy.HTTPError(error, message=message) + + if not varkw and varkw_usage > 0: + + # If there were extra query string parameters, it's a 404 Not Found + extra_qs_params = set(qs_params).intersection(extra_kwargs) + if extra_qs_params: + message = None + if show_mismatched_params: + message="Unexpected query string "\ + "parameters: %s" % ", ".join(extra_qs_params) + raise cherrypy.HTTPError(404, message=message) + + # If there were any extra body parameters, it's a 400 Not Found + extra_body_params = set(body_params).intersection(extra_kwargs) + if extra_body_params: + message = None + if show_mismatched_params: + message="Unexpected body parameters: "\ + "%s" % ", ".join(extra_body_params) + raise cherrypy.HTTPError(400, message=message) + + +try: + import inspect +except ImportError: + test_callable_spec = lambda callable, args, kwargs: None + + + +class LateParamPageHandler(PageHandler): + """When passing cherrypy.request.params to the page handler, we do not + want to capture that dict too early; we want to give tools like the + decoding tool a chance to modify the params dict in-between the lookup + of the handler and the actual calling of the handler. This subclass + takes that into account, and allows request.params to be 'bound late' + (it's more complicated than that, but that's the effect). + """ + + def _get_kwargs(self): + kwargs = cherrypy.serving.request.params.copy() + if self._kwargs: + kwargs.update(self._kwargs) + return kwargs + + def _set_kwargs(self, kwargs): + self._kwargs = kwargs + + kwargs = property(_get_kwargs, _set_kwargs, + doc='page handler kwargs (with ' + 'cherrypy.request.params copied in)') + + +if sys.version_info < (3, 0): + punctuation_to_underscores = string.maketrans( + string.punctuation, '_' * len(string.punctuation)) + def validate_translator(t): + if not isinstance(t, str) or len(t) != 256: + raise ValueError("The translate argument must be a str of len 256.") +else: + punctuation_to_underscores = str.maketrans( + string.punctuation, '_' * len(string.punctuation)) + def validate_translator(t): + if not isinstance(t, dict): + raise ValueError("The translate argument must be a dict.") + +class Dispatcher(object): + """CherryPy Dispatcher which walks a tree of objects to find a handler. + + The tree is rooted at cherrypy.request.app.root, and each hierarchical + component in the path_info argument is matched to a corresponding nested + attribute of the root object. Matching handlers must have an 'exposed' + attribute which evaluates to True. The special method name "index" + matches a URI which ends in a slash ("/"). The special method name + "default" may match a portion of the path_info (but only when no longer + substring of the path_info matches some other object). + + This is the default, built-in dispatcher for CherryPy. + """ + + dispatch_method_name = '_cp_dispatch' + """ + The name of the dispatch method that nodes may optionally implement + to provide their own dynamic dispatch algorithm. + """ + + def __init__(self, dispatch_method_name=None, + translate=punctuation_to_underscores): + validate_translator(translate) + self.translate = translate + if dispatch_method_name: + self.dispatch_method_name = dispatch_method_name + + def __call__(self, path_info): + """Set handler and config for the current request.""" + request = cherrypy.serving.request + func, vpath = self.find_handler(path_info) + + if func: + # Decode any leftover %2F in the virtual_path atoms. + vpath = [x.replace("%2F", "/") for x in vpath] + request.handler = LateParamPageHandler(func, *vpath) + else: + request.handler = cherrypy.NotFound() + + def find_handler(self, path): + """Return the appropriate page handler, plus any virtual path. + + This will return two objects. The first will be a callable, + which can be used to generate page output. Any parameters from + the query string or request body will be sent to that callable + as keyword arguments. + + The callable is found by traversing the application's tree, + starting from cherrypy.request.app.root, and matching path + components to successive objects in the tree. For example, the + URL "/path/to/handler" might return root.path.to.handler. + + The second object returned will be a list of names which are + 'virtual path' components: parts of the URL which are dynamic, + and were not used when looking up the handler. + These virtual path components are passed to the handler as + positional arguments. + """ + request = cherrypy.serving.request + app = request.app + root = app.root + dispatch_name = self.dispatch_method_name + + # Get config for the root object/path. + fullpath = [x for x in path.strip('/').split('/') if x] + ['index'] + fullpath_len = len(fullpath) + segleft = fullpath_len + nodeconf = {} + if hasattr(root, "_cp_config"): + nodeconf.update(root._cp_config) + if "/" in app.config: + nodeconf.update(app.config["/"]) + object_trail = [['root', root, nodeconf, segleft]] + + node = root + iternames = fullpath[:] + while iternames: + name = iternames[0] + # map to legal Python identifiers (e.g. replace '.' with '_') + objname = name.translate(self.translate) + + nodeconf = {} + subnode = getattr(node, objname, None) + pre_len = len(iternames) + if subnode is None: + dispatch = getattr(node, dispatch_name, None) + if dispatch and hasattr(dispatch, '__call__') and not \ + getattr(dispatch, 'exposed', False) and \ + pre_len > 1: + #Don't expose the hidden 'index' token to _cp_dispatch + #We skip this if pre_len == 1 since it makes no sense + #to call a dispatcher when we have no tokens left. + index_name = iternames.pop() + subnode = dispatch(vpath=iternames) + iternames.append(index_name) + else: + #We didn't find a path, but keep processing in case there + #is a default() handler. + iternames.pop(0) + else: + #We found the path, remove the vpath entry + iternames.pop(0) + segleft = len(iternames) + if segleft > pre_len: + #No path segment was removed. Raise an error. + raise cherrypy.CherryPyException( + "A vpath segment was added. Custom dispatchers may only " + + "remove elements. While trying to process " + + "{0} in {1}".format(name, fullpath) + ) + elif segleft == pre_len: + #Assume that the handler used the current path segment, but + #did not pop it. This allows things like + #return getattr(self, vpath[0], None) + iternames.pop(0) + segleft -= 1 + node = subnode + + if node is not None: + # Get _cp_config attached to this node. + if hasattr(node, "_cp_config"): + nodeconf.update(node._cp_config) + + # Mix in values from app.config for this path. + existing_len = fullpath_len - pre_len + if existing_len != 0: + curpath = '/' + '/'.join(fullpath[0:existing_len]) + else: + curpath = '' + new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft] + for seg in new_segs: + curpath += '/' + seg + if curpath in app.config: + nodeconf.update(app.config[curpath]) + + object_trail.append([name, node, nodeconf, segleft]) + + def set_conf(): + """Collapse all object_trail config into cherrypy.request.config.""" + base = cherrypy.config.copy() + # Note that we merge the config from each node + # even if that node was None. + for name, obj, conf, segleft in object_trail: + base.update(conf) + if 'tools.staticdir.dir' in conf: + base['tools.staticdir.section'] = '/' + '/'.join(fullpath[0:fullpath_len - segleft]) + return base + + # Try successive objects (reverse order) + num_candidates = len(object_trail) - 1 + for i in range(num_candidates, -1, -1): + + name, candidate, nodeconf, segleft = object_trail[i] + if candidate is None: + continue + + # Try a "default" method on the current leaf. + if hasattr(candidate, "default"): + defhandler = candidate.default + if getattr(defhandler, 'exposed', False): + # Insert any extra _cp_config from the default handler. + conf = getattr(defhandler, "_cp_config", {}) + object_trail.insert(i+1, ["default", defhandler, conf, segleft]) + request.config = set_conf() + # See http://www.cherrypy.org/ticket/613 + request.is_index = path.endswith("/") + return defhandler, fullpath[fullpath_len - segleft:-1] + + # Uncomment the next line to restrict positional params to "default". + # if i < num_candidates - 2: continue + + # Try the current leaf. + if getattr(candidate, 'exposed', False): + request.config = set_conf() + if i == num_candidates: + # We found the extra ".index". Mark request so tools + # can redirect if path_info has no trailing slash. + request.is_index = True + else: + # We're not at an 'index' handler. Mark request so tools + # can redirect if path_info has NO trailing slash. + # Note that this also includes handlers which take + # positional parameters (virtual paths). + request.is_index = False + return candidate, fullpath[fullpath_len - segleft:-1] + + # We didn't find anything + request.config = set_conf() + return None, [] + + +class MethodDispatcher(Dispatcher): + """Additional dispatch based on cherrypy.request.method.upper(). + + Methods named GET, POST, etc will be called on an exposed class. + The method names must be all caps; the appropriate Allow header + will be output showing all capitalized method names as allowable + HTTP verbs. + + Note that the containing class must be exposed, not the methods. + """ + + def __call__(self, path_info): + """Set handler and config for the current request.""" + request = cherrypy.serving.request + resource, vpath = self.find_handler(path_info) + + if resource: + # Set Allow header + avail = [m for m in dir(resource) if m.isupper()] + if "GET" in avail and "HEAD" not in avail: + avail.append("HEAD") + avail.sort() + cherrypy.serving.response.headers['Allow'] = ", ".join(avail) + + # Find the subhandler + meth = request.method.upper() + func = getattr(resource, meth, None) + if func is None and meth == "HEAD": + func = getattr(resource, "GET", None) + if func: + # Grab any _cp_config on the subhandler. + if hasattr(func, "_cp_config"): + request.config.update(func._cp_config) + + # Decode any leftover %2F in the virtual_path atoms. + vpath = [x.replace("%2F", "/") for x in vpath] + request.handler = LateParamPageHandler(func, *vpath) + else: + request.handler = cherrypy.HTTPError(405) + else: + request.handler = cherrypy.NotFound() + + +class RoutesDispatcher(object): + """A Routes based dispatcher for CherryPy.""" + + def __init__(self, full_result=False): + """ + Routes dispatcher + + Set full_result to True if you wish the controller + and the action to be passed on to the page handler + parameters. By default they won't be. + """ + import routes + self.full_result = full_result + self.controllers = {} + self.mapper = routes.Mapper() + self.mapper.controller_scan = self.controllers.keys + + def connect(self, name, route, controller, **kwargs): + self.controllers[name] = controller + self.mapper.connect(name, route, controller=name, **kwargs) + + def redirect(self, url): + raise cherrypy.HTTPRedirect(url) + + def __call__(self, path_info): + """Set handler and config for the current request.""" + func = self.find_handler(path_info) + if func: + cherrypy.serving.request.handler = LateParamPageHandler(func) + else: + cherrypy.serving.request.handler = cherrypy.NotFound() + + def find_handler(self, path_info): + """Find the right page handler, and set request.config.""" + import routes + + request = cherrypy.serving.request + + config = routes.request_config() + config.mapper = self.mapper + if hasattr(request, 'wsgi_environ'): + config.environ = request.wsgi_environ + config.host = request.headers.get('Host', None) + config.protocol = request.scheme + config.redirect = self.redirect + + result = self.mapper.match(path_info) + + config.mapper_dict = result + params = {} + if result: + params = result.copy() + if not self.full_result: + params.pop('controller', None) + params.pop('action', None) + request.params.update(params) + + # Get config for the root object/path. + request.config = base = cherrypy.config.copy() + curpath = "" + + def merge(nodeconf): + if 'tools.staticdir.dir' in nodeconf: + nodeconf['tools.staticdir.section'] = curpath or "/" + base.update(nodeconf) + + app = request.app + root = app.root + if hasattr(root, "_cp_config"): + merge(root._cp_config) + if "/" in app.config: + merge(app.config["/"]) + + # Mix in values from app.config. + atoms = [x for x in path_info.split("/") if x] + if atoms: + last = atoms.pop() + else: + last = None + for atom in atoms: + curpath = "/".join((curpath, atom)) + if curpath in app.config: + merge(app.config[curpath]) + + handler = None + if result: + controller = result.get('controller') + controller = self.controllers.get(controller, controller) + if controller: + if isinstance(controller, classtype): + controller = controller() + # Get config from the controller. + if hasattr(controller, "_cp_config"): + merge(controller._cp_config) + + action = result.get('action') + if action is not None: + handler = getattr(controller, action, None) + # Get config from the handler + if hasattr(handler, "_cp_config"): + merge(handler._cp_config) + else: + handler = controller + + # Do the last path atom here so it can + # override the controller's _cp_config. + if last: + curpath = "/".join((curpath, last)) + if curpath in app.config: + merge(app.config[curpath]) + + return handler + + +def XMLRPCDispatcher(next_dispatcher=Dispatcher()): + from cherrypy.lib import xmlrpcutil + def xmlrpc_dispatch(path_info): + path_info = xmlrpcutil.patched_path(path_info) + return next_dispatcher(path_info) + return xmlrpc_dispatch + + +def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, **domains): + """ + Select a different handler based on the Host header. + + This can be useful when running multiple sites within one CP server. + It allows several domains to point to different parts of a single + website structure. For example:: + + http://www.domain.example -> root + http://www.domain2.example -> root/domain2/ + http://www.domain2.example:443 -> root/secure + + can be accomplished via the following config:: + + [/] + request.dispatch = cherrypy.dispatch.VirtualHost( + **{'www.domain2.example': '/domain2', + 'www.domain2.example:443': '/secure', + }) + + next_dispatcher + The next dispatcher object in the dispatch chain. + The VirtualHost dispatcher adds a prefix to the URL and calls + another dispatcher. Defaults to cherrypy.dispatch.Dispatcher(). + + use_x_forwarded_host + If True (the default), any "X-Forwarded-Host" + request header will be used instead of the "Host" header. This + is commonly added by HTTP servers (such as Apache) when proxying. + + ``**domains`` + A dict of {host header value: virtual prefix} pairs. + The incoming "Host" request header is looked up in this dict, + and, if a match is found, the corresponding "virtual prefix" + value will be prepended to the URL path before calling the + next dispatcher. Note that you often need separate entries + for "example.com" and "www.example.com". In addition, "Host" + headers may contain the port number. + """ + from cherrypy.lib import httputil + def vhost_dispatch(path_info): + request = cherrypy.serving.request + header = request.headers.get + + domain = header('Host', '') + if use_x_forwarded_host: + domain = header("X-Forwarded-Host", domain) + + prefix = domains.get(domain, "") + if prefix: + path_info = httputil.urljoin(prefix, path_info) + + result = next_dispatcher(path_info) + + # Touch up staticdir config. See http://www.cherrypy.org/ticket/614. + section = request.config.get('tools.staticdir.section') + if section: + section = section[len(prefix):] + request.config['tools.staticdir.section'] = section + + return result + return vhost_dispatch + diff --git a/libs/cherrypy/_cperror.py b/libs/cherrypy/_cperror.py new file mode 100644 index 0000000..76a409f --- /dev/null +++ b/libs/cherrypy/_cperror.py @@ -0,0 +1,556 @@ +"""Exception classes for CherryPy. + +CherryPy provides (and uses) exceptions for declaring that the HTTP response +should be a status other than the default "200 OK". You can ``raise`` them like +normal Python exceptions. You can also call them and they will raise themselves; +this means you can set an :class:`HTTPError` +or :class:`HTTPRedirect` as the +:attr:`request.handler`. + +.. _redirectingpost: + +Redirecting POST +================ + +When you GET a resource and are redirected by the server to another Location, +there's generally no problem since GET is both a "safe method" (there should +be no side-effects) and an "idempotent method" (multiple calls are no different +than a single call). + +POST, however, is neither safe nor idempotent--if you +charge a credit card, you don't want to be charged twice by a redirect! + +For this reason, *none* of the 3xx responses permit a user-agent (browser) to +resubmit a POST on redirection without first confirming the action with the user: + +===== ================================= =========== +300 Multiple Choices Confirm with the user +301 Moved Permanently Confirm with the user +302 Found (Object moved temporarily) Confirm with the user +303 See Other GET the new URI--no confirmation +304 Not modified (for conditional GET only--POST should not raise this error) +305 Use Proxy Confirm with the user +307 Temporary Redirect Confirm with the user +===== ================================= =========== + +However, browsers have historically implemented these restrictions poorly; +in particular, many browsers do not force the user to confirm 301, 302 +or 307 when redirecting POST. For this reason, CherryPy defaults to 303, +which most user-agents appear to have implemented correctly. Therefore, if +you raise HTTPRedirect for a POST request, the user-agent will most likely +attempt to GET the new URI (without asking for confirmation from the user). +We realize this is confusing for developers, but it's the safest thing we +could do. You are of course free to raise ``HTTPRedirect(uri, status=302)`` +or any other 3xx status if you know what you're doing, but given the +environment, we couldn't let any of those be the default. + +Custom Error Handling +===================== + +.. image:: /refman/cperrors.gif + +Anticipated HTTP responses +-------------------------- + +The 'error_page' config namespace can be used to provide custom HTML output for +expected responses (like 404 Not Found). Supply a filename from which the output +will be read. The contents will be interpolated with the values %(status)s, +%(message)s, %(traceback)s, and %(version)s using plain old Python +`string formatting `_. + +:: + + _cp_config = {'error_page.404': os.path.join(localDir, "static/index.html")} + + +Beginning in version 3.1, you may also provide a function or other callable as +an error_page entry. It will be passed the same status, message, traceback and +version arguments that are interpolated into templates:: + + def error_page_402(status, message, traceback, version): + return "Error %s - Well, I'm very sorry but you haven't paid!" % status + cherrypy.config.update({'error_page.402': error_page_402}) + +Also in 3.1, in addition to the numbered error codes, you may also supply +"error_page.default" to handle all codes which do not have their own error_page entry. + + + +Unanticipated errors +-------------------- + +CherryPy also has a generic error handling mechanism: whenever an unanticipated +error occurs in your code, it will call +:func:`Request.error_response` to set +the response status, headers, and body. By default, this is the same output as +:class:`HTTPError(500) `. If you want to provide +some other behavior, you generally replace "request.error_response". + +Here is some sample code that shows how to display a custom error message and +send an e-mail containing the error:: + + from cherrypy import _cperror + + def handle_error(): + cherrypy.response.status = 500 + cherrypy.response.body = ["Sorry, an error occured"] + sendMail('error@domain.com', 'Error in your web app', _cperror.format_exc()) + + class Root: + _cp_config = {'request.error_response': handle_error} + + +Note that you have to explicitly set :attr:`response.body ` +and not simply return an error message as a result. +""" + +from cgi import escape as _escape +from sys import exc_info as _exc_info +from traceback import format_exception as _format_exception +from cherrypy._cpcompat import basestring, bytestr, iteritems, ntob, tonative, urljoin as _urljoin +from cherrypy.lib import httputil as _httputil + + +class CherryPyException(Exception): + """A base class for CherryPy exceptions.""" + pass + + +class TimeoutError(CherryPyException): + """Exception raised when Response.timed_out is detected.""" + pass + + +class InternalRedirect(CherryPyException): + """Exception raised to switch to the handler for a different URL. + + This exception will redirect processing to another path within the site + (without informing the client). Provide the new path as an argument when + raising the exception. Provide any params in the querystring for the new URL. + """ + + def __init__(self, path, query_string=""): + import cherrypy + self.request = cherrypy.serving.request + + self.query_string = query_string + if "?" in path: + # Separate any params included in the path + path, self.query_string = path.split("?", 1) + + # Note that urljoin will "do the right thing" whether url is: + # 1. a URL relative to root (e.g. "/dummy") + # 2. a URL relative to the current path + # Note that any query string will be discarded. + path = _urljoin(self.request.path_info, path) + + # Set a 'path' member attribute so that code which traps this + # error can have access to it. + self.path = path + + CherryPyException.__init__(self, path, self.query_string) + + +class HTTPRedirect(CherryPyException): + """Exception raised when the request should be redirected. + + This exception will force a HTTP redirect to the URL or URL's you give it. + The new URL must be passed as the first argument to the Exception, + e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list. + If a URL is absolute, it will be used as-is. If it is relative, it is + assumed to be relative to the current cherrypy.request.path_info. + + If one of the provided URL is a unicode object, it will be encoded + using the default encoding or the one passed in parameter. + + There are multiple types of redirect, from which you can select via the + ``status`` argument. If you do not provide a ``status`` arg, it defaults to + 303 (or 302 if responding with HTTP/1.0). + + Examples:: + + raise cherrypy.HTTPRedirect("") + raise cherrypy.HTTPRedirect("/abs/path", 307) + raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301) + + See :ref:`redirectingpost` for additional caveats. + """ + + status = None + """The integer HTTP status code to emit.""" + + urls = None + """The list of URL's to emit.""" + + encoding = 'utf-8' + """The encoding when passed urls are not native strings""" + + def __init__(self, urls, status=None, encoding=None): + import cherrypy + request = cherrypy.serving.request + + if isinstance(urls, basestring): + urls = [urls] + + abs_urls = [] + for url in urls: + url = tonative(url, encoding or self.encoding) + + # Note that urljoin will "do the right thing" whether url is: + # 1. a complete URL with host (e.g. "http://www.example.com/test") + # 2. a URL relative to root (e.g. "/dummy") + # 3. a URL relative to the current path + # Note that any query string in cherrypy.request is discarded. + url = _urljoin(cherrypy.url(), url) + abs_urls.append(url) + self.urls = abs_urls + + # RFC 2616 indicates a 301 response code fits our goal; however, + # browser support for 301 is quite messy. Do 302/303 instead. See + # http://www.alanflavell.org.uk/www/post-redirect.html + if status is None: + if request.protocol >= (1, 1): + status = 303 + else: + status = 302 + else: + status = int(status) + if status < 300 or status > 399: + raise ValueError("status must be between 300 and 399.") + + self.status = status + CherryPyException.__init__(self, abs_urls, status) + + def set_response(self): + """Modify cherrypy.response status, headers, and body to represent self. + + CherryPy uses this internally, but you can also use it to create an + HTTPRedirect object and set its output without *raising* the exception. + """ + import cherrypy + response = cherrypy.serving.response + response.status = status = self.status + + if status in (300, 301, 302, 303, 307): + response.headers['Content-Type'] = "text/html;charset=utf-8" + # "The ... URI SHOULD be given by the Location field + # in the response." + response.headers['Location'] = self.urls[0] + + # "Unless the request method was HEAD, the entity of the response + # SHOULD contain a short hypertext note with a hyperlink to the + # new URI(s)." + msg = {300: "This resource can be found at %s.", + 301: "This resource has permanently moved to %s.", + 302: "This resource resides temporarily at %s.", + 303: "This resource can be found at %s.", + 307: "This resource has moved temporarily to %s.", + }[status] + msgs = [msg % (u, u) for u in self.urls] + response.body = ntob("
\n".join(msgs), 'utf-8') + # Previous code may have set C-L, so we have to reset it + # (allow finalize to set it). + response.headers.pop('Content-Length', None) + elif status == 304: + # Not Modified. + # "The response MUST include the following header fields: + # Date, unless its omission is required by section 14.18.1" + # The "Date" header should have been set in Response.__init__ + + # "...the response SHOULD NOT include other entity-headers." + for key in ('Allow', 'Content-Encoding', 'Content-Language', + 'Content-Length', 'Content-Location', 'Content-MD5', + 'Content-Range', 'Content-Type', 'Expires', + 'Last-Modified'): + if key in response.headers: + del response.headers[key] + + # "The 304 response MUST NOT contain a message-body." + response.body = None + # Previous code may have set C-L, so we have to reset it. + response.headers.pop('Content-Length', None) + elif status == 305: + # Use Proxy. + # self.urls[0] should be the URI of the proxy. + response.headers['Location'] = self.urls[0] + response.body = None + # Previous code may have set C-L, so we have to reset it. + response.headers.pop('Content-Length', None) + else: + raise ValueError("The %s status code is unknown." % status) + + def __call__(self): + """Use this exception as a request.handler (raise self).""" + raise self + + +def clean_headers(status): + """Remove any headers which should not apply to an error response.""" + import cherrypy + + response = cherrypy.serving.response + + # Remove headers which applied to the original content, + # but do not apply to the error page. + respheaders = response.headers + for key in ["Accept-Ranges", "Age", "ETag", "Location", "Retry-After", + "Vary", "Content-Encoding", "Content-Length", "Expires", + "Content-Location", "Content-MD5", "Last-Modified"]: + if key in respheaders: + del respheaders[key] + + if status != 416: + # A server sending a response with status code 416 (Requested + # range not satisfiable) SHOULD include a Content-Range field + # with a byte-range-resp-spec of "*". The instance-length + # specifies the current length of the selected resource. + # A response with status code 206 (Partial Content) MUST NOT + # include a Content-Range field with a byte-range- resp-spec of "*". + if "Content-Range" in respheaders: + del respheaders["Content-Range"] + + +class HTTPError(CherryPyException): + """Exception used to return an HTTP error code (4xx-5xx) to the client. + + This exception can be used to automatically send a response using a http status + code, with an appropriate error page. It takes an optional + ``status`` argument (which must be between 400 and 599); it defaults to 500 + ("Internal Server Error"). It also takes an optional ``message`` argument, + which will be returned in the response body. See + `RFC 2616 `_ + for a complete list of available error codes and when to use them. + + Examples:: + + raise cherrypy.HTTPError(403) + raise cherrypy.HTTPError("403 Forbidden", "You are not allowed to access this resource.") + """ + + status = None + """The HTTP status code. May be of type int or str (with a Reason-Phrase).""" + + code = None + """The integer HTTP status code.""" + + reason = None + """The HTTP Reason-Phrase string.""" + + def __init__(self, status=500, message=None): + self.status = status + try: + self.code, self.reason, defaultmsg = _httputil.valid_status(status) + except ValueError: + raise self.__class__(500, _exc_info()[1].args[0]) + + if self.code < 400 or self.code > 599: + raise ValueError("status must be between 400 and 599.") + + # See http://www.python.org/dev/peps/pep-0352/ + # self.message = message + self._message = message or defaultmsg + CherryPyException.__init__(self, status, message) + + def set_response(self): + """Modify cherrypy.response status, headers, and body to represent self. + + CherryPy uses this internally, but you can also use it to create an + HTTPError object and set its output without *raising* the exception. + """ + import cherrypy + + response = cherrypy.serving.response + + clean_headers(self.code) + + # In all cases, finalize will be called after this method, + # so don't bother cleaning up response values here. + response.status = self.status + tb = None + if cherrypy.serving.request.show_tracebacks: + tb = format_exc() + response.headers['Content-Type'] = "text/html;charset=utf-8" + response.headers.pop('Content-Length', None) + + content = ntob(self.get_error_page(self.status, traceback=tb, + message=self._message), 'utf-8') + response.body = content + + _be_ie_unfriendly(self.code) + + def get_error_page(self, *args, **kwargs): + return get_error_page(*args, **kwargs) + + def __call__(self): + """Use this exception as a request.handler (raise self).""" + raise self + + +class NotFound(HTTPError): + """Exception raised when a URL could not be mapped to any handler (404). + + This is equivalent to raising + :class:`HTTPError("404 Not Found") `. + """ + + def __init__(self, path=None): + if path is None: + import cherrypy + request = cherrypy.serving.request + path = request.script_name + request.path_info + self.args = (path,) + HTTPError.__init__(self, 404, "The path '%s' was not found." % path) + + +_HTTPErrorTemplate = ''' + + + + %(status)s + + + +

%(status)s

+

%(message)s

+
%(traceback)s
+
+ Powered by CherryPy %(version)s +
+ + +''' + +def get_error_page(status, **kwargs): + """Return an HTML page, containing a pretty error response. + + status should be an int or a str. + kwargs will be interpolated into the page template. + """ + import cherrypy + + try: + code, reason, message = _httputil.valid_status(status) + except ValueError: + raise cherrypy.HTTPError(500, _exc_info()[1].args[0]) + + # We can't use setdefault here, because some + # callers send None for kwarg values. + if kwargs.get('status') is None: + kwargs['status'] = "%s %s" % (code, reason) + if kwargs.get('message') is None: + kwargs['message'] = message + if kwargs.get('traceback') is None: + kwargs['traceback'] = '' + if kwargs.get('version') is None: + kwargs['version'] = cherrypy.__version__ + + for k, v in iteritems(kwargs): + if v is None: + kwargs[k] = "" + else: + kwargs[k] = _escape(kwargs[k]) + + # Use a custom template or callable for the error page? + pages = cherrypy.serving.request.error_page + error_page = pages.get(code) or pages.get('default') + if error_page: + try: + if hasattr(error_page, '__call__'): + return error_page(**kwargs) + else: + data = open(error_page, 'rb').read() + return tonative(data) % kwargs + except: + e = _format_exception(*_exc_info())[-1] + m = kwargs['message'] + if m: + m += "
" + m += "In addition, the custom error page failed:\n
%s" % e + kwargs['message'] = m + + return _HTTPErrorTemplate % kwargs + + +_ie_friendly_error_sizes = { + 400: 512, 403: 256, 404: 512, 405: 256, + 406: 512, 408: 512, 409: 512, 410: 256, + 500: 512, 501: 512, 505: 512, + } + + +def _be_ie_unfriendly(status): + import cherrypy + response = cherrypy.serving.response + + # For some statuses, Internet Explorer 5+ shows "friendly error + # messages" instead of our response.body if the body is smaller + # than a given size. Fix this by returning a body over that size + # (by adding whitespace). + # See http://support.microsoft.com/kb/q218155/ + s = _ie_friendly_error_sizes.get(status, 0) + if s: + s += 1 + # Since we are issuing an HTTP error status, we assume that + # the entity is short, and we should just collapse it. + content = response.collapse_body() + l = len(content) + if l and l < s: + # IN ADDITION: the response must be written to IE + # in one chunk or it will still get replaced! Bah. + content = content + (ntob(" ") * (s - l)) + response.body = content + response.headers['Content-Length'] = str(len(content)) + + +def format_exc(exc=None): + """Return exc (or sys.exc_info if None), formatted.""" + try: + if exc is None: + exc = _exc_info() + if exc == (None, None, None): + return "" + import traceback + return "".join(traceback.format_exception(*exc)) + finally: + del exc + +def bare_error(extrabody=None): + """Produce status, headers, body for a critical error. + + Returns a triple without calling any other questionable functions, + so it should be as error-free as possible. Call it from an HTTP server + if you get errors outside of the request. + + If extrabody is None, a friendly but rather unhelpful error message + is set in the body. If extrabody is a string, it will be appended + as-is to the body. + """ + + # The whole point of this function is to be a last line-of-defense + # in handling errors. That is, it must not raise any errors itself; + # it cannot be allowed to fail. Therefore, don't add to it! + # In particular, don't call any other CP functions. + + body = ntob("Unrecoverable error in the server.") + if extrabody is not None: + if not isinstance(extrabody, bytestr): + extrabody = extrabody.encode('utf-8') + body += ntob("\n") + extrabody + + return (ntob("500 Internal Server Error"), + [(ntob('Content-Type'), ntob('text/plain')), + (ntob('Content-Length'), ntob(str(len(body)),'ISO-8859-1'))], + [body]) + + diff --git a/libs/cherrypy/_cplogging.py b/libs/cherrypy/_cplogging.py new file mode 100644 index 0000000..e10c942 --- /dev/null +++ b/libs/cherrypy/_cplogging.py @@ -0,0 +1,440 @@ +""" +Simple config +============= + +Although CherryPy uses the :mod:`Python logging module `, it does so +behind the scenes so that simple logging is simple, but complicated logging +is still possible. "Simple" logging means that you can log to the screen +(i.e. console/stdout) or to a file, and that you can easily have separate +error and access log files. + +Here are the simplified logging settings. You use these by adding lines to +your config file or dict. You should set these at either the global level or +per application (see next), but generally not both. + + * ``log.screen``: Set this to True to have both "error" and "access" messages + printed to stdout. + * ``log.access_file``: Set this to an absolute filename where you want + "access" messages written. + * ``log.error_file``: Set this to an absolute filename where you want "error" + messages written. + +Many events are automatically logged; to log your own application events, call +:func:`cherrypy.log`. + +Architecture +============ + +Separate scopes +--------------- + +CherryPy provides log managers at both the global and application layers. +This means you can have one set of logging rules for your entire site, +and another set of rules specific to each application. The global log +manager is found at :func:`cherrypy.log`, and the log manager for each +application is found at :attr:`app.log`. +If you're inside a request, the latter is reachable from +``cherrypy.request.app.log``; if you're outside a request, you'll have to obtain +a reference to the ``app``: either the return value of +:func:`tree.mount()` or, if you used +:func:`quickstart()` instead, via ``cherrypy.tree.apps['/']``. + +By default, the global logs are named "cherrypy.error" and "cherrypy.access", +and the application logs are named "cherrypy.error.2378745" and +"cherrypy.access.2378745" (the number is the id of the Application object). +This means that the application logs "bubble up" to the site logs, so if your +application has no log handlers, the site-level handlers will still log the +messages. + +Errors vs. Access +----------------- + +Each log manager handles both "access" messages (one per HTTP request) and +"error" messages (everything else). Note that the "error" log is not just for +errors! The format of access messages is highly formalized, but the error log +isn't--it receives messages from a variety of sources (including full error +tracebacks, if enabled). + + +Custom Handlers +=============== + +The simple settings above work by manipulating Python's standard :mod:`logging` +module. So when you need something more complex, the full power of the standard +module is yours to exploit. You can borrow or create custom handlers, formats, +filters, and much more. Here's an example that skips the standard FileHandler +and uses a RotatingFileHandler instead: + +:: + + #python + log = app.log + + # Remove the default FileHandlers if present. + log.error_file = "" + log.access_file = "" + + maxBytes = getattr(log, "rot_maxBytes", 10000000) + backupCount = getattr(log, "rot_backupCount", 1000) + + # Make a new RotatingFileHandler for the error log. + fname = getattr(log, "rot_error_file", "error.log") + h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount) + h.setLevel(DEBUG) + h.setFormatter(_cplogging.logfmt) + log.error_log.addHandler(h) + + # Make a new RotatingFileHandler for the access log. + fname = getattr(log, "rot_access_file", "access.log") + h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount) + h.setLevel(DEBUG) + h.setFormatter(_cplogging.logfmt) + log.access_log.addHandler(h) + + +The ``rot_*`` attributes are pulled straight from the application log object. +Since "log.*" config entries simply set attributes on the log object, you can +add custom attributes to your heart's content. Note that these handlers are +used ''instead'' of the default, simple handlers outlined above (so don't set +the "log.error_file" config entry, for example). +""" + +import datetime +import logging +# Silence the no-handlers "warning" (stderr write!) in stdlib logging +logging.Logger.manager.emittedNoHandlerWarning = 1 +logfmt = logging.Formatter("%(message)s") +import os +import sys + +import cherrypy +from cherrypy import _cperror +from cherrypy._cpcompat import ntob, py3k + + +class NullHandler(logging.Handler): + """A no-op logging handler to silence the logging.lastResort handler.""" + + def handle(self, record): + pass + + def emit(self, record): + pass + + def createLock(self): + self.lock = None + + +class LogManager(object): + """An object to assist both simple and advanced logging. + + ``cherrypy.log`` is an instance of this class. + """ + + appid = None + """The id() of the Application object which owns this log manager. If this + is a global log manager, appid is None.""" + + error_log = None + """The actual :class:`logging.Logger` instance for error messages.""" + + access_log = None + """The actual :class:`logging.Logger` instance for access messages.""" + + if py3k: + access_log_format = \ + '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"' + else: + access_log_format = \ + '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' + + logger_root = None + """The "top-level" logger name. + + This string will be used as the first segment in the Logger names. + The default is "cherrypy", for example, in which case the Logger names + will be of the form:: + + cherrypy.error. + cherrypy.access. + """ + + def __init__(self, appid=None, logger_root="cherrypy"): + self.logger_root = logger_root + self.appid = appid + if appid is None: + self.error_log = logging.getLogger("%s.error" % logger_root) + self.access_log = logging.getLogger("%s.access" % logger_root) + else: + self.error_log = logging.getLogger("%s.error.%s" % (logger_root, appid)) + self.access_log = logging.getLogger("%s.access.%s" % (logger_root, appid)) + self.error_log.setLevel(logging.INFO) + self.access_log.setLevel(logging.INFO) + + # Silence the no-handlers "warning" (stderr write!) in stdlib logging + self.error_log.addHandler(NullHandler()) + self.access_log.addHandler(NullHandler()) + + cherrypy.engine.subscribe('graceful', self.reopen_files) + + def reopen_files(self): + """Close and reopen all file handlers.""" + for log in (self.error_log, self.access_log): + for h in log.handlers: + if isinstance(h, logging.FileHandler): + h.acquire() + h.stream.close() + h.stream = open(h.baseFilename, h.mode) + h.release() + + def error(self, msg='', context='', severity=logging.INFO, traceback=False): + """Write the given ``msg`` to the error log. + + This is not just for errors! Applications may call this at any time + to log application-specific information. + + If ``traceback`` is True, the traceback of the current exception + (if any) will be appended to ``msg``. + """ + if traceback: + msg += _cperror.format_exc() + self.error_log.log(severity, ' '.join((self.time(), context, msg))) + + def __call__(self, *args, **kwargs): + """An alias for ``error``.""" + return self.error(*args, **kwargs) + + def access(self): + """Write to the access log (in Apache/NCSA Combined Log format). + + See http://httpd.apache.org/docs/2.0/logs.html#combined for format + details. + + CherryPy calls this automatically for you. Note there are no arguments; + it collects the data itself from + :class:`cherrypy.request`. + + Like Apache started doing in 2.0.46, non-printable and other special + characters in %r (and we expand that to all parts) are escaped using + \\xhh sequences, where hh stands for the hexadecimal representation + of the raw byte. Exceptions from this rule are " and \\, which are + escaped by prepending a backslash, and all whitespace characters, + which are written in their C-style notation (\\n, \\t, etc). + """ + request = cherrypy.serving.request + remote = request.remote + response = cherrypy.serving.response + outheaders = response.headers + inheaders = request.headers + if response.output_status is None: + status = "-" + else: + status = response.output_status.split(ntob(" "), 1)[0] + if py3k: + status = status.decode('ISO-8859-1') + + atoms = {'h': remote.name or remote.ip, + 'l': '-', + 'u': getattr(request, "login", None) or "-", + 't': self.time(), + 'r': request.request_line, + 's': status, + 'b': dict.get(outheaders, 'Content-Length', '') or "-", + 'f': dict.get(inheaders, 'Referer', ''), + 'a': dict.get(inheaders, 'User-Agent', ''), + } + if py3k: + for k, v in atoms.items(): + if not isinstance(v, str): + v = str(v) + v = v.replace('"', '\\"').encode('utf8') + # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc + # and backslash for us. All we have to do is strip the quotes. + v = repr(v)[2:-1] + + # in python 3.0 the repr of bytes (as returned by encode) + # uses double \'s. But then the logger escapes them yet, again + # resulting in quadruple slashes. Remove the extra one here. + v = v.replace('\\\\', '\\') + + # Escape double-quote. + atoms[k] = v + + try: + self.access_log.log(logging.INFO, self.access_log_format.format(**atoms)) + except: + self(traceback=True) + else: + for k, v in atoms.items(): + if isinstance(v, unicode): + v = v.encode('utf8') + elif not isinstance(v, str): + v = str(v) + # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc + # and backslash for us. All we have to do is strip the quotes. + v = repr(v)[1:-1] + # Escape double-quote. + atoms[k] = v.replace('"', '\\"') + + try: + self.access_log.log(logging.INFO, self.access_log_format % atoms) + except: + self(traceback=True) + + def time(self): + """Return now() in Apache Common Log Format (no timezone).""" + now = datetime.datetime.now() + monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', + 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] + month = monthnames[now.month - 1].capitalize() + return ('[%02d/%s/%04d:%02d:%02d:%02d]' % + (now.day, month, now.year, now.hour, now.minute, now.second)) + + def _get_builtin_handler(self, log, key): + for h in log.handlers: + if getattr(h, "_cpbuiltin", None) == key: + return h + + + # ------------------------- Screen handlers ------------------------- # + + def _set_screen_handler(self, log, enable, stream=None): + h = self._get_builtin_handler(log, "screen") + if enable: + if not h: + if stream is None: + stream=sys.stderr + h = logging.StreamHandler(stream) + h.setFormatter(logfmt) + h._cpbuiltin = "screen" + log.addHandler(h) + elif h: + log.handlers.remove(h) + + def _get_screen(self): + h = self._get_builtin_handler + has_h = h(self.error_log, "screen") or h(self.access_log, "screen") + return bool(has_h) + + def _set_screen(self, newvalue): + self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr) + self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout) + screen = property(_get_screen, _set_screen, + doc="""Turn stderr/stdout logging on or off. + + If you set this to True, it'll add the appropriate StreamHandler for + you. If you set it to False, it will remove the handler. + """) + + # -------------------------- File handlers -------------------------- # + + def _add_builtin_file_handler(self, log, fname): + h = logging.FileHandler(fname) + h.setFormatter(logfmt) + h._cpbuiltin = "file" + log.addHandler(h) + + def _set_file_handler(self, log, filename): + h = self._get_builtin_handler(log, "file") + if filename: + if h: + if h.baseFilename != os.path.abspath(filename): + h.close() + log.handlers.remove(h) + self._add_builtin_file_handler(log, filename) + else: + self._add_builtin_file_handler(log, filename) + else: + if h: + h.close() + log.handlers.remove(h) + + def _get_error_file(self): + h = self._get_builtin_handler(self.error_log, "file") + if h: + return h.baseFilename + return '' + def _set_error_file(self, newvalue): + self._set_file_handler(self.error_log, newvalue) + error_file = property(_get_error_file, _set_error_file, + doc="""The filename for self.error_log. + + If you set this to a string, it'll add the appropriate FileHandler for + you. If you set it to ``None`` or ``''``, it will remove the handler. + """) + + def _get_access_file(self): + h = self._get_builtin_handler(self.access_log, "file") + if h: + return h.baseFilename + return '' + def _set_access_file(self, newvalue): + self._set_file_handler(self.access_log, newvalue) + access_file = property(_get_access_file, _set_access_file, + doc="""The filename for self.access_log. + + If you set this to a string, it'll add the appropriate FileHandler for + you. If you set it to ``None`` or ``''``, it will remove the handler. + """) + + # ------------------------- WSGI handlers ------------------------- # + + def _set_wsgi_handler(self, log, enable): + h = self._get_builtin_handler(log, "wsgi") + if enable: + if not h: + h = WSGIErrorHandler() + h.setFormatter(logfmt) + h._cpbuiltin = "wsgi" + log.addHandler(h) + elif h: + log.handlers.remove(h) + + def _get_wsgi(self): + return bool(self._get_builtin_handler(self.error_log, "wsgi")) + + def _set_wsgi(self, newvalue): + self._set_wsgi_handler(self.error_log, newvalue) + wsgi = property(_get_wsgi, _set_wsgi, + doc="""Write errors to wsgi.errors. + + If you set this to True, it'll add the appropriate + :class:`WSGIErrorHandler` for you + (which writes errors to ``wsgi.errors``). + If you set it to False, it will remove the handler. + """) + + +class WSGIErrorHandler(logging.Handler): + "A handler class which writes logging records to environ['wsgi.errors']." + + def flush(self): + """Flushes the stream.""" + try: + stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors') + except (AttributeError, KeyError): + pass + else: + stream.flush() + + def emit(self, record): + """Emit a record.""" + try: + stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors') + except (AttributeError, KeyError): + pass + else: + try: + msg = self.format(record) + fs = "%s\n" + import types + if not hasattr(types, "UnicodeType"): #if no unicode support... + stream.write(fs % msg) + else: + try: + stream.write(fs % msg) + except UnicodeError: + stream.write(fs % msg.encode("UTF-8")) + self.flush() + except: + self.handleError(record) diff --git a/libs/cherrypy/_cpmodpy.py b/libs/cherrypy/_cpmodpy.py new file mode 100644 index 0000000..76ef6ea --- /dev/null +++ b/libs/cherrypy/_cpmodpy.py @@ -0,0 +1,344 @@ +"""Native adapter for serving CherryPy via mod_python + +Basic usage: + +########################################## +# Application in a module called myapp.py +########################################## + +import cherrypy + +class Root: + @cherrypy.expose + def index(self): + return 'Hi there, Ho there, Hey there' + + +# We will use this method from the mod_python configuration +# as the entry point to our application +def setup_server(): + cherrypy.tree.mount(Root()) + cherrypy.config.update({'environment': 'production', + 'log.screen': False, + 'show_tracebacks': False}) + +########################################## +# mod_python settings for apache2 +# This should reside in your httpd.conf +# or a file that will be loaded at +# apache startup +########################################## + +# Start +DocumentRoot "/" +Listen 8080 +LoadModule python_module /usr/lib/apache2/modules/mod_python.so + + + PythonPath "sys.path+['/path/to/my/application']" + SetHandler python-program + PythonHandler cherrypy._cpmodpy::handler + PythonOption cherrypy.setup myapp::setup_server + PythonDebug On + +# End + +The actual path to your mod_python.so is dependent on your +environment. In this case we suppose a global mod_python +installation on a Linux distribution such as Ubuntu. + +We do set the PythonPath configuration setting so that +your application can be found by from the user running +the apache2 instance. Of course if your application +resides in the global site-package this won't be needed. + +Then restart apache2 and access http://127.0.0.1:8080 +""" + +import logging +import sys + +import cherrypy +from cherrypy._cpcompat import BytesIO, copyitems, ntob +from cherrypy._cperror import format_exc, bare_error +from cherrypy.lib import httputil + + +# ------------------------------ Request-handling + + + +def setup(req): + from mod_python import apache + + # Run any setup functions defined by a "PythonOption cherrypy.setup" directive. + options = req.get_options() + if 'cherrypy.setup' in options: + for function in options['cherrypy.setup'].split(): + atoms = function.split('::', 1) + if len(atoms) == 1: + mod = __import__(atoms[0], globals(), locals()) + else: + modname, fname = atoms + mod = __import__(modname, globals(), locals(), [fname]) + func = getattr(mod, fname) + func() + + cherrypy.config.update({'log.screen': False, + "tools.ignore_headers.on": True, + "tools.ignore_headers.headers": ['Range'], + }) + + engine = cherrypy.engine + if hasattr(engine, "signal_handler"): + engine.signal_handler.unsubscribe() + if hasattr(engine, "console_control_handler"): + engine.console_control_handler.unsubscribe() + engine.autoreload.unsubscribe() + cherrypy.server.unsubscribe() + + def _log(msg, level): + newlevel = apache.APLOG_ERR + if logging.DEBUG >= level: + newlevel = apache.APLOG_DEBUG + elif logging.INFO >= level: + newlevel = apache.APLOG_INFO + elif logging.WARNING >= level: + newlevel = apache.APLOG_WARNING + # On Windows, req.server is required or the msg will vanish. See + # http://www.modpython.org/pipermail/mod_python/2003-October/014291.html. + # Also, "When server is not specified...LogLevel does not apply..." + apache.log_error(msg, newlevel, req.server) + engine.subscribe('log', _log) + + engine.start() + + def cherrypy_cleanup(data): + engine.exit() + try: + # apache.register_cleanup wasn't available until 3.1.4. + apache.register_cleanup(cherrypy_cleanup) + except AttributeError: + req.server.register_cleanup(req, cherrypy_cleanup) + + +class _ReadOnlyRequest: + expose = ('read', 'readline', 'readlines') + def __init__(self, req): + for method in self.expose: + self.__dict__[method] = getattr(req, method) + + +recursive = False + +_isSetUp = False +def handler(req): + from mod_python import apache + try: + global _isSetUp + if not _isSetUp: + setup(req) + _isSetUp = True + + # Obtain a Request object from CherryPy + local = req.connection.local_addr + local = httputil.Host(local[0], local[1], req.connection.local_host or "") + remote = req.connection.remote_addr + remote = httputil.Host(remote[0], remote[1], req.connection.remote_host or "") + + scheme = req.parsed_uri[0] or 'http' + req.get_basic_auth_pw() + + try: + # apache.mpm_query only became available in mod_python 3.1 + q = apache.mpm_query + threaded = q(apache.AP_MPMQ_IS_THREADED) + forked = q(apache.AP_MPMQ_IS_FORKED) + except AttributeError: + bad_value = ("You must provide a PythonOption '%s', " + "either 'on' or 'off', when running a version " + "of mod_python < 3.1") + + threaded = options.get('multithread', '').lower() + if threaded == 'on': + threaded = True + elif threaded == 'off': + threaded = False + else: + raise ValueError(bad_value % "multithread") + + forked = options.get('multiprocess', '').lower() + if forked == 'on': + forked = True + elif forked == 'off': + forked = False + else: + raise ValueError(bad_value % "multiprocess") + + sn = cherrypy.tree.script_name(req.uri or "/") + if sn is None: + send_response(req, '404 Not Found', [], '') + else: + app = cherrypy.tree.apps[sn] + method = req.method + path = req.uri + qs = req.args or "" + reqproto = req.protocol + headers = copyitems(req.headers_in) + rfile = _ReadOnlyRequest(req) + prev = None + + try: + redirections = [] + while True: + request, response = app.get_serving(local, remote, scheme, + "HTTP/1.1") + request.login = req.user + request.multithread = bool(threaded) + request.multiprocess = bool(forked) + request.app = app + request.prev = prev + + # Run the CherryPy Request object and obtain the response + try: + request.run(method, path, qs, reqproto, headers, rfile) + break + except cherrypy.InternalRedirect: + ir = sys.exc_info()[1] + app.release_serving() + prev = request + + if not recursive: + if ir.path in redirections: + raise RuntimeError("InternalRedirector visited the " + "same URL twice: %r" % ir.path) + else: + # Add the *previous* path_info + qs to redirections. + if qs: + qs = "?" + qs + redirections.append(sn + path + qs) + + # Munge environment and try again. + method = "GET" + path = ir.path + qs = ir.query_string + rfile = BytesIO() + + send_response(req, response.output_status, response.header_list, + response.body, response.stream) + finally: + app.release_serving() + except: + tb = format_exc() + cherrypy.log(tb, 'MOD_PYTHON', severity=logging.ERROR) + s, h, b = bare_error() + send_response(req, s, h, b) + return apache.OK + + +def send_response(req, status, headers, body, stream=False): + # Set response status + req.status = int(status[:3]) + + # Set response headers + req.content_type = "text/plain" + for header, value in headers: + if header.lower() == 'content-type': + req.content_type = value + continue + req.headers_out.add(header, value) + + if stream: + # Flush now so the status and headers are sent immediately. + req.flush() + + # Set response body + if isinstance(body, basestring): + req.write(body) + else: + for seg in body: + req.write(seg) + + + +# --------------- Startup tools for CherryPy + mod_python --------------- # + + +import os +import re +try: + import subprocess + def popen(fullcmd): + p = subprocess.Popen(fullcmd, shell=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + close_fds=True) + return p.stdout +except ImportError: + def popen(fullcmd): + pipein, pipeout = os.popen4(fullcmd) + return pipeout + + +def read_process(cmd, args=""): + fullcmd = "%s %s" % (cmd, args) + pipeout = popen(fullcmd) + try: + firstline = pipeout.readline() + if (re.search(ntob("(not recognized|No such file|not found)"), firstline, + re.IGNORECASE)): + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +class ModPythonServer(object): + + template = """ +# Apache2 server configuration file for running CherryPy with mod_python. + +DocumentRoot "/" +Listen %(port)s +LoadModule python_module modules/mod_python.so + + + SetHandler python-program + PythonHandler %(handler)s + PythonDebug On +%(opts)s + +""" + + def __init__(self, loc="/", port=80, opts=None, apache_path="apache", + handler="cherrypy._cpmodpy::handler"): + self.loc = loc + self.port = port + self.opts = opts + self.apache_path = apache_path + self.handler = handler + + def start(self): + opts = "".join([" PythonOption %s %s\n" % (k, v) + for k, v in self.opts]) + conf_data = self.template % {"port": self.port, + "loc": self.loc, + "opts": opts, + "handler": self.handler, + } + + mpconf = os.path.join(os.path.dirname(__file__), "cpmodpy.conf") + f = open(mpconf, 'wb') + try: + f.write(conf_data) + finally: + f.close() + + response = read_process(self.apache_path, "-k start -f %s" % mpconf) + self.ready = True + return response + + def stop(self): + os.popen("apache -k stop") + self.ready = False + diff --git a/libs/cherrypy/_cpnative_server.py b/libs/cherrypy/_cpnative_server.py new file mode 100644 index 0000000..57f715a --- /dev/null +++ b/libs/cherrypy/_cpnative_server.py @@ -0,0 +1,149 @@ +"""Native adapter for serving CherryPy via its builtin server.""" + +import logging +import sys + +import cherrypy +from cherrypy._cpcompat import BytesIO +from cherrypy._cperror import format_exc, bare_error +from cherrypy.lib import httputil +from cherrypy import wsgiserver + + +class NativeGateway(wsgiserver.Gateway): + + recursive = False + + def respond(self): + req = self.req + try: + # Obtain a Request object from CherryPy + local = req.server.bind_addr + local = httputil.Host(local[0], local[1], "") + remote = req.conn.remote_addr, req.conn.remote_port + remote = httputil.Host(remote[0], remote[1], "") + + scheme = req.scheme + sn = cherrypy.tree.script_name(req.uri or "/") + if sn is None: + self.send_response('404 Not Found', [], ['']) + else: + app = cherrypy.tree.apps[sn] + method = req.method + path = req.path + qs = req.qs or "" + headers = req.inheaders.items() + rfile = req.rfile + prev = None + + try: + redirections = [] + while True: + request, response = app.get_serving( + local, remote, scheme, "HTTP/1.1") + request.multithread = True + request.multiprocess = False + request.app = app + request.prev = prev + + # Run the CherryPy Request object and obtain the response + try: + request.run(method, path, qs, req.request_protocol, headers, rfile) + break + except cherrypy.InternalRedirect: + ir = sys.exc_info()[1] + app.release_serving() + prev = request + + if not self.recursive: + if ir.path in redirections: + raise RuntimeError("InternalRedirector visited the " + "same URL twice: %r" % ir.path) + else: + # Add the *previous* path_info + qs to redirections. + if qs: + qs = "?" + qs + redirections.append(sn + path + qs) + + # Munge environment and try again. + method = "GET" + path = ir.path + qs = ir.query_string + rfile = BytesIO() + + self.send_response( + response.output_status, response.header_list, + response.body) + finally: + app.release_serving() + except: + tb = format_exc() + #print tb + cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR) + s, h, b = bare_error() + self.send_response(s, h, b) + + def send_response(self, status, headers, body): + req = self.req + + # Set response status + req.status = str(status or "500 Server Error") + + # Set response headers + for header, value in headers: + req.outheaders.append((header, value)) + if (req.ready and not req.sent_headers): + req.sent_headers = True + req.send_headers() + + # Set response body + for seg in body: + req.write(seg) + + +class CPHTTPServer(wsgiserver.HTTPServer): + """Wrapper for wsgiserver.HTTPServer. + + wsgiserver has been designed to not reference CherryPy in any way, + so that it can be used in other frameworks and applications. + Therefore, we wrap it here, so we can apply some attributes + from config -> cherrypy.server -> HTTPServer. + """ + + def __init__(self, server_adapter=cherrypy.server): + self.server_adapter = server_adapter + + server_name = (self.server_adapter.socket_host or + self.server_adapter.socket_file or + None) + + wsgiserver.HTTPServer.__init__( + self, server_adapter.bind_addr, NativeGateway, + minthreads=server_adapter.thread_pool, + maxthreads=server_adapter.thread_pool_max, + server_name=server_name) + + self.max_request_header_size = self.server_adapter.max_request_header_size or 0 + self.max_request_body_size = self.server_adapter.max_request_body_size or 0 + self.request_queue_size = self.server_adapter.socket_queue_size + self.timeout = self.server_adapter.socket_timeout + self.shutdown_timeout = self.server_adapter.shutdown_timeout + self.protocol = self.server_adapter.protocol_version + self.nodelay = self.server_adapter.nodelay + + ssl_module = self.server_adapter.ssl_module or 'pyopenssl' + if self.server_adapter.ssl_context: + adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain) + self.ssl_adapter.context = self.server_adapter.ssl_context + elif self.server_adapter.ssl_certificate: + adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain) + + diff --git a/libs/cherrypy/_cpreqbody.py b/libs/cherrypy/_cpreqbody.py new file mode 100644 index 0000000..5d72c85 --- /dev/null +++ b/libs/cherrypy/_cpreqbody.py @@ -0,0 +1,965 @@ +"""Request body processing for CherryPy. + +.. versionadded:: 3.2 + +Application authors have complete control over the parsing of HTTP request +entities. In short, :attr:`cherrypy.request.body` +is now always set to an instance of :class:`RequestBody`, +and *that* class is a subclass of :class:`Entity`. + +When an HTTP request includes an entity body, it is often desirable to +provide that information to applications in a form other than the raw bytes. +Different content types demand different approaches. Examples: + + * For a GIF file, we want the raw bytes in a stream. + * An HTML form is better parsed into its component fields, and each text field + decoded from bytes to unicode. + * A JSON body should be deserialized into a Python dict or list. + +When the request contains a Content-Type header, the media type is used as a +key to look up a value in the +:attr:`request.body.processors` dict. +If the full media +type is not found, then the major type is tried; for example, if no processor +is found for the 'image/jpeg' type, then we look for a processor for the 'image' +types altogether. If neither the full type nor the major type has a matching +processor, then a default processor is used +(:func:`default_proc`). For most +types, this means no processing is done, and the body is left unread as a +raw byte stream. Processors are configurable in an 'on_start_resource' hook. + +Some processors, especially those for the 'text' types, attempt to decode bytes +to unicode. If the Content-Type request header includes a 'charset' parameter, +this is used to decode the entity. Otherwise, one or more default charsets may +be attempted, although this decision is up to each processor. If a processor +successfully decodes an Entity or Part, it should set the +:attr:`charset` attribute +on the Entity or Part to the name of the successful charset, so that +applications can easily re-encode or transcode the value if they wish. + +If the Content-Type of the request entity is of major type 'multipart', then +the above parsing process, and possibly a decoding process, is performed for +each part. + +For both the full entity and multipart parts, a Content-Disposition header may +be used to fill :attr:`name` and +:attr:`filename` attributes on the +request.body or the Part. + +.. _custombodyprocessors: + +Custom Processors +================= + +You can add your own processors for any specific or major MIME type. Simply add +it to the :attr:`processors` dict in a +hook/tool that runs at ``on_start_resource`` or ``before_request_body``. +Here's the built-in JSON tool for an example:: + + def json_in(force=True, debug=False): + request = cherrypy.serving.request + def json_processor(entity): + \"""Read application/json data into request.json.\""" + if not entity.headers.get("Content-Length", ""): + raise cherrypy.HTTPError(411) + + body = entity.fp.read() + try: + request.json = json_decode(body) + except ValueError: + raise cherrypy.HTTPError(400, 'Invalid JSON document') + if force: + request.body.processors.clear() + request.body.default_proc = cherrypy.HTTPError( + 415, 'Expected an application/json content type') + request.body.processors['application/json'] = json_processor + +We begin by defining a new ``json_processor`` function to stick in the ``processors`` +dictionary. All processor functions take a single argument, the ``Entity`` instance +they are to process. It will be called whenever a request is received (for those +URI's where the tool is turned on) which has a ``Content-Type`` of +"application/json". + +First, it checks for a valid ``Content-Length`` (raising 411 if not valid), then +reads the remaining bytes on the socket. The ``fp`` object knows its own length, so +it won't hang waiting for data that never arrives. It will return when all data +has been read. Then, we decode those bytes using Python's built-in ``json`` module, +and stick the decoded result onto ``request.json`` . If it cannot be decoded, we +raise 400. + +If the "force" argument is True (the default), the ``Tool`` clears the ``processors`` +dict so that request entities of other ``Content-Types`` aren't parsed at all. Since +there's no entry for those invalid MIME types, the ``default_proc`` method of ``cherrypy.request.body`` +is called. But this does nothing by default (usually to provide the page handler an opportunity to handle it.) +But in our case, we want to raise 415, so we replace ``request.body.default_proc`` +with the error (``HTTPError`` instances, when called, raise themselves). + +If we were defining a custom processor, we can do so without making a ``Tool``. Just add the config entry:: + + request.body.processors = {'application/json': json_processor} + +Note that you can only replace the ``processors`` dict wholesale this way, not update the existing one. +""" + +try: + from io import DEFAULT_BUFFER_SIZE +except ImportError: + DEFAULT_BUFFER_SIZE = 8192 +import re +import sys +import tempfile +try: + from urllib import unquote_plus +except ImportError: + def unquote_plus(bs): + """Bytes version of urllib.parse.unquote_plus.""" + bs = bs.replace(ntob('+'), ntob(' ')) + atoms = bs.split(ntob('%')) + for i in range(1, len(atoms)): + item = atoms[i] + try: + pct = int(item[:2], 16) + atoms[i] = bytes([pct]) + item[2:] + except ValueError: + pass + return ntob('').join(atoms) + +import cherrypy +from cherrypy._cpcompat import basestring, ntob, ntou +from cherrypy.lib import httputil + + +# -------------------------------- Processors -------------------------------- # + +def process_urlencoded(entity): + """Read application/x-www-form-urlencoded data into entity.params.""" + qs = entity.fp.read() + for charset in entity.attempt_charsets: + try: + params = {} + for aparam in qs.split(ntob('&')): + for pair in aparam.split(ntob(';')): + if not pair: + continue + + atoms = pair.split(ntob('='), 1) + if len(atoms) == 1: + atoms.append(ntob('')) + + key = unquote_plus(atoms[0]).decode(charset) + value = unquote_plus(atoms[1]).decode(charset) + + if key in params: + if not isinstance(params[key], list): + params[key] = [params[key]] + params[key].append(value) + else: + params[key] = value + except UnicodeDecodeError: + pass + else: + entity.charset = charset + break + else: + raise cherrypy.HTTPError( + 400, "The request entity could not be decoded. The following " + "charsets were attempted: %s" % repr(entity.attempt_charsets)) + + # Now that all values have been successfully parsed and decoded, + # apply them to the entity.params dict. + for key, value in params.items(): + if key in entity.params: + if not isinstance(entity.params[key], list): + entity.params[key] = [entity.params[key]] + entity.params[key].append(value) + else: + entity.params[key] = value + + +def process_multipart(entity): + """Read all multipart parts into entity.parts.""" + ib = "" + if 'boundary' in entity.content_type.params: + # http://tools.ietf.org/html/rfc2046#section-5.1.1 + # "The grammar for parameters on the Content-type field is such that it + # is often necessary to enclose the boundary parameter values in quotes + # on the Content-type line" + ib = entity.content_type.params['boundary'].strip('"') + + if not re.match("^[ -~]{0,200}[!-~]$", ib): + raise ValueError('Invalid boundary in multipart form: %r' % (ib,)) + + ib = ('--' + ib).encode('ascii') + + # Find the first marker + while True: + b = entity.readline() + if not b: + return + + b = b.strip() + if b == ib: + break + + # Read all parts + while True: + part = entity.part_class.from_fp(entity.fp, ib) + entity.parts.append(part) + part.process() + if part.fp.done: + break + +def process_multipart_form_data(entity): + """Read all multipart/form-data parts into entity.parts or entity.params.""" + process_multipart(entity) + + kept_parts = [] + for part in entity.parts: + if part.name is None: + kept_parts.append(part) + else: + if part.filename is None: + # It's a regular field + value = part.fullvalue() + else: + # It's a file upload. Retain the whole part so consumer code + # has access to its .file and .filename attributes. + value = part + + if part.name in entity.params: + if not isinstance(entity.params[part.name], list): + entity.params[part.name] = [entity.params[part.name]] + entity.params[part.name].append(value) + else: + entity.params[part.name] = value + + entity.parts = kept_parts + +def _old_process_multipart(entity): + """The behavior of 3.2 and lower. Deprecated and will be changed in 3.3.""" + process_multipart(entity) + + params = entity.params + + for part in entity.parts: + if part.name is None: + key = ntou('parts') + else: + key = part.name + + if part.filename is None: + # It's a regular field + value = part.fullvalue() + else: + # It's a file upload. Retain the whole part so consumer code + # has access to its .file and .filename attributes. + value = part + + if key in params: + if not isinstance(params[key], list): + params[key] = [params[key]] + params[key].append(value) + else: + params[key] = value + + + +# --------------------------------- Entities --------------------------------- # + + +class Entity(object): + """An HTTP request body, or MIME multipart body. + + This class collects information about the HTTP request entity. When a + given entity is of MIME type "multipart", each part is parsed into its own + Entity instance, and the set of parts stored in + :attr:`entity.parts`. + + Between the ``before_request_body`` and ``before_handler`` tools, CherryPy + tries to process the request body (if any) by calling + :func:`request.body.process`, a dict. + If a matching processor cannot be found for the complete Content-Type, + it tries again using the major type. For example, if a request with an + entity of type "image/jpeg" arrives, but no processor can be found for + that complete type, then one is sought for the major type "image". If a + processor is still not found, then the + :func:`default_proc` method of the + Entity is called (which does nothing by default; you can override this too). + + CherryPy includes processors for the "application/x-www-form-urlencoded" + type, the "multipart/form-data" type, and the "multipart" major type. + CherryPy 3.2 processes these types almost exactly as older versions. + Parts are passed as arguments to the page handler using their + ``Content-Disposition.name`` if given, otherwise in a generic "parts" + argument. Each such part is either a string, or the + :class:`Part` itself if it's a file. (In this + case it will have ``file`` and ``filename`` attributes, or possibly a + ``value`` attribute). Each Part is itself a subclass of + Entity, and has its own ``process`` method and ``processors`` dict. + + There is a separate processor for the "multipart" major type which is more + flexible, and simply stores all multipart parts in + :attr:`request.body.parts`. You can + enable it with:: + + cherrypy.request.body.processors['multipart'] = _cpreqbody.process_multipart + + in an ``on_start_resource`` tool. + """ + + # http://tools.ietf.org/html/rfc2046#section-4.1.2: + # "The default character set, which must be assumed in the + # absence of a charset parameter, is US-ASCII." + # However, many browsers send data in utf-8 with no charset. + attempt_charsets = ['utf-8'] + """A list of strings, each of which should be a known encoding. + + When the Content-Type of the request body warrants it, each of the given + encodings will be tried in order. The first one to successfully decode the + entity without raising an error is stored as + :attr:`entity.charset`. This defaults + to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by + `HTTP/1.1 `_), + but ``['us-ascii', 'utf-8']`` for multipart parts. + """ + + charset = None + """The successful decoding; see "attempt_charsets" above.""" + + content_type = None + """The value of the Content-Type request header. + + If the Entity is part of a multipart payload, this will be the Content-Type + given in the MIME headers for this part. + """ + + default_content_type = 'application/x-www-form-urlencoded' + """This defines a default ``Content-Type`` to use if no Content-Type header + is given. The empty string is used for RequestBody, which results in the + request body not being read or parsed at all. This is by design; a missing + ``Content-Type`` header in the HTTP request entity is an error at best, + and a security hole at worst. For multipart parts, however, the MIME spec + declares that a part with no Content-Type defaults to "text/plain" + (see :class:`Part`). + """ + + filename = None + """The ``Content-Disposition.filename`` header, if available.""" + + fp = None + """The readable socket file object.""" + + headers = None + """A dict of request/multipart header names and values. + + This is a copy of the ``request.headers`` for the ``request.body``; + for multipart parts, it is the set of headers for that part. + """ + + length = None + """The value of the ``Content-Length`` header, if provided.""" + + name = None + """The "name" parameter of the ``Content-Disposition`` header, if any.""" + + params = None + """ + If the request Content-Type is 'application/x-www-form-urlencoded' or + multipart, this will be a dict of the params pulled from the entity + body; that is, it will be the portion of request.params that come + from the message body (sometimes called "POST params", although they + can be sent with various HTTP method verbs). This value is set between + the 'before_request_body' and 'before_handler' hooks (assuming that + process_request_body is True).""" + + processors = {'application/x-www-form-urlencoded': process_urlencoded, + 'multipart/form-data': process_multipart_form_data, + 'multipart': process_multipart, + } + """A dict of Content-Type names to processor methods.""" + + parts = None + """A list of Part instances if ``Content-Type`` is of major type "multipart".""" + + part_class = None + """The class used for multipart parts. + + You can replace this with custom subclasses to alter the processing of + multipart parts. + """ + + def __init__(self, fp, headers, params=None, parts=None): + # Make an instance-specific copy of the class processors + # so Tools, etc. can replace them per-request. + self.processors = self.processors.copy() + + self.fp = fp + self.headers = headers + + if params is None: + params = {} + self.params = params + + if parts is None: + parts = [] + self.parts = parts + + # Content-Type + self.content_type = headers.elements('Content-Type') + if self.content_type: + self.content_type = self.content_type[0] + else: + self.content_type = httputil.HeaderElement.from_str( + self.default_content_type) + + # Copy the class 'attempt_charsets', prepending any Content-Type charset + dec = self.content_type.params.get("charset", None) + if dec: + self.attempt_charsets = [dec] + [c for c in self.attempt_charsets + if c != dec] + else: + self.attempt_charsets = self.attempt_charsets[:] + + # Length + self.length = None + clen = headers.get('Content-Length', None) + # If Transfer-Encoding is 'chunked', ignore any Content-Length. + if clen is not None and 'chunked' not in headers.get('Transfer-Encoding', ''): + try: + self.length = int(clen) + except ValueError: + pass + + # Content-Disposition + self.name = None + self.filename = None + disp = headers.elements('Content-Disposition') + if disp: + disp = disp[0] + if 'name' in disp.params: + self.name = disp.params['name'] + if self.name.startswith('"') and self.name.endswith('"'): + self.name = self.name[1:-1] + if 'filename' in disp.params: + self.filename = disp.params['filename'] + if self.filename.startswith('"') and self.filename.endswith('"'): + self.filename = self.filename[1:-1] + + # The 'type' attribute is deprecated in 3.2; remove it in 3.3. + type = property(lambda self: self.content_type, + doc="""A deprecated alias for :attr:`content_type`.""") + + def read(self, size=None, fp_out=None): + return self.fp.read(size, fp_out) + + def readline(self, size=None): + return self.fp.readline(size) + + def readlines(self, sizehint=None): + return self.fp.readlines(sizehint) + + def __iter__(self): + return self + + def __next__(self): + line = self.readline() + if not line: + raise StopIteration + return line + + def next(self): + return self.__next__() + + def read_into_file(self, fp_out=None): + """Read the request body into fp_out (or make_file() if None). Return fp_out.""" + if fp_out is None: + fp_out = self.make_file() + self.read(fp_out=fp_out) + return fp_out + + def make_file(self): + """Return a file-like object into which the request body will be read. + + By default, this will return a TemporaryFile. Override as needed. + See also :attr:`cherrypy._cpreqbody.Part.maxrambytes`.""" + return tempfile.TemporaryFile() + + def fullvalue(self): + """Return this entity as a string, whether stored in a file or not.""" + if self.file: + # It was stored in a tempfile. Read it. + self.file.seek(0) + value = self.file.read() + self.file.seek(0) + else: + value = self.value + return value + + def process(self): + """Execute the best-match processor for the given media type.""" + proc = None + ct = self.content_type.value + try: + proc = self.processors[ct] + except KeyError: + toptype = ct.split('/', 1)[0] + try: + proc = self.processors[toptype] + except KeyError: + pass + if proc is None: + self.default_proc() + else: + proc(self) + + def default_proc(self): + """Called if a more-specific processor is not found for the ``Content-Type``.""" + # Leave the fp alone for someone else to read. This works fine + # for request.body, but the Part subclasses need to override this + # so they can move on to the next part. + pass + + +class Part(Entity): + """A MIME part entity, part of a multipart entity.""" + + # "The default character set, which must be assumed in the absence of a + # charset parameter, is US-ASCII." + attempt_charsets = ['us-ascii', 'utf-8'] + """A list of strings, each of which should be a known encoding. + + When the Content-Type of the request body warrants it, each of the given + encodings will be tried in order. The first one to successfully decode the + entity without raising an error is stored as + :attr:`entity.charset`. This defaults + to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by + `HTTP/1.1 `_), + but ``['us-ascii', 'utf-8']`` for multipart parts. + """ + + boundary = None + """The MIME multipart boundary.""" + + default_content_type = 'text/plain' + """This defines a default ``Content-Type`` to use if no Content-Type header + is given. The empty string is used for RequestBody, which results in the + request body not being read or parsed at all. This is by design; a missing + ``Content-Type`` header in the HTTP request entity is an error at best, + and a security hole at worst. For multipart parts, however (this class), + the MIME spec declares that a part with no Content-Type defaults to + "text/plain". + """ + + # This is the default in stdlib cgi. We may want to increase it. + maxrambytes = 1000 + """The threshold of bytes after which point the ``Part`` will store its data + in a file (generated by :func:`make_file`) + instead of a string. Defaults to 1000, just like the :mod:`cgi` module in + Python's standard library. + """ + + def __init__(self, fp, headers, boundary): + Entity.__init__(self, fp, headers) + self.boundary = boundary + self.file = None + self.value = None + + def from_fp(cls, fp, boundary): + headers = cls.read_headers(fp) + return cls(fp, headers, boundary) + from_fp = classmethod(from_fp) + + def read_headers(cls, fp): + headers = httputil.HeaderMap() + while True: + line = fp.readline() + if not line: + # No more data--illegal end of headers + raise EOFError("Illegal end of headers.") + + if line == ntob('\r\n'): + # Normal end of headers + break + if not line.endswith(ntob('\r\n')): + raise ValueError("MIME requires CRLF terminators: %r" % line) + + if line[0] in ntob(' \t'): + # It's a continuation line. + v = line.strip().decode('ISO-8859-1') + else: + k, v = line.split(ntob(":"), 1) + k = k.strip().decode('ISO-8859-1') + v = v.strip().decode('ISO-8859-1') + + existing = headers.get(k) + if existing: + v = ", ".join((existing, v)) + headers[k] = v + + return headers + read_headers = classmethod(read_headers) + + def read_lines_to_boundary(self, fp_out=None): + """Read bytes from self.fp and return or write them to a file. + + If the 'fp_out' argument is None (the default), all bytes read are + returned in a single byte string. + + If the 'fp_out' argument is not None, it must be a file-like object that + supports the 'write' method; all bytes read will be written to the fp, + and that fp is returned. + """ + endmarker = self.boundary + ntob("--") + delim = ntob("") + prev_lf = True + lines = [] + seen = 0 + while True: + line = self.fp.readline(1<<16) + if not line: + raise EOFError("Illegal end of multipart body.") + if line.startswith(ntob("--")) and prev_lf: + strippedline = line.strip() + if strippedline == self.boundary: + break + if strippedline == endmarker: + self.fp.finish() + break + + line = delim + line + + if line.endswith(ntob("\r\n")): + delim = ntob("\r\n") + line = line[:-2] + prev_lf = True + elif line.endswith(ntob("\n")): + delim = ntob("\n") + line = line[:-1] + prev_lf = True + else: + delim = ntob("") + prev_lf = False + + if fp_out is None: + lines.append(line) + seen += len(line) + if seen > self.maxrambytes: + fp_out = self.make_file() + for line in lines: + fp_out.write(line) + else: + fp_out.write(line) + + if fp_out is None: + result = ntob('').join(lines) + for charset in self.attempt_charsets: + try: + result = result.decode(charset) + except UnicodeDecodeError: + pass + else: + self.charset = charset + return result + else: + raise cherrypy.HTTPError( + 400, "The request entity could not be decoded. The following " + "charsets were attempted: %s" % repr(self.attempt_charsets)) + else: + fp_out.seek(0) + return fp_out + + def default_proc(self): + """Called if a more-specific processor is not found for the ``Content-Type``.""" + if self.filename: + # Always read into a file if a .filename was given. + self.file = self.read_into_file() + else: + result = self.read_lines_to_boundary() + if isinstance(result, basestring): + self.value = result + else: + self.file = result + + def read_into_file(self, fp_out=None): + """Read the request body into fp_out (or make_file() if None). Return fp_out.""" + if fp_out is None: + fp_out = self.make_file() + self.read_lines_to_boundary(fp_out=fp_out) + return fp_out + +Entity.part_class = Part + +try: + inf = float('inf') +except ValueError: + # Python 2.4 and lower + class Infinity(object): + def __cmp__(self, other): + return 1 + def __sub__(self, other): + return self + inf = Infinity() + + +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'] + + +class SizedReader: + + def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE, has_trailers=False): + # Wrap our fp in a buffer so peek() works + self.fp = fp + self.length = length + self.maxbytes = maxbytes + self.buffer = ntob('') + self.bufsize = bufsize + self.bytes_read = 0 + self.done = False + self.has_trailers = has_trailers + + def read(self, size=None, fp_out=None): + """Read bytes from the request body and return or write them to a file. + + A number of bytes less than or equal to the 'size' argument are read + off the socket. The actual number of bytes read are tracked in + self.bytes_read. The number may be smaller than 'size' when 1) the + client sends fewer bytes, 2) the 'Content-Length' request header + specifies fewer bytes than requested, or 3) the number of bytes read + exceeds self.maxbytes (in which case, 413 is raised). + + If the 'fp_out' argument is None (the default), all bytes read are + returned in a single byte string. + + If the 'fp_out' argument is not None, it must be a file-like object that + supports the 'write' method; all bytes read will be written to the fp, + and None is returned. + """ + + if self.length is None: + if size is None: + remaining = inf + else: + remaining = size + else: + remaining = self.length - self.bytes_read + if size and size < remaining: + remaining = size + if remaining == 0: + self.finish() + if fp_out is None: + return ntob('') + else: + return None + + chunks = [] + + # Read bytes from the buffer. + if self.buffer: + if remaining is inf: + data = self.buffer + self.buffer = ntob('') + else: + data = self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + datalen = len(data) + remaining -= datalen + + # Check lengths. + self.bytes_read += datalen + if self.maxbytes and self.bytes_read > self.maxbytes: + raise cherrypy.HTTPError(413) + + # Store the data. + if fp_out is None: + chunks.append(data) + else: + fp_out.write(data) + + # Read bytes from the socket. + while remaining > 0: + chunksize = min(remaining, self.bufsize) + try: + data = self.fp.read(chunksize) + except Exception: + e = sys.exc_info()[1] + if e.__class__.__name__ == 'MaxSizeExceeded': + # Post data is too big + raise cherrypy.HTTPError( + 413, "Maximum request length: %r" % e.args[1]) + else: + raise + if not data: + self.finish() + break + datalen = len(data) + remaining -= datalen + + # Check lengths. + self.bytes_read += datalen + if self.maxbytes and self.bytes_read > self.maxbytes: + raise cherrypy.HTTPError(413) + + # Store the data. + if fp_out is None: + chunks.append(data) + else: + fp_out.write(data) + + if fp_out is None: + return ntob('').join(chunks) + + def readline(self, size=None): + """Read a line from the request body and return it.""" + chunks = [] + while size is None or size > 0: + chunksize = self.bufsize + if size is not None and size < self.bufsize: + chunksize = size + data = self.read(chunksize) + if not data: + break + pos = data.find(ntob('\n')) + 1 + if pos: + chunks.append(data[:pos]) + remainder = data[pos:] + self.buffer += remainder + self.bytes_read -= len(remainder) + break + else: + chunks.append(data) + return ntob('').join(chunks) + + def readlines(self, sizehint=None): + """Read lines from the request body and return them.""" + if self.length is not None: + if sizehint is None: + sizehint = self.length - self.bytes_read + else: + sizehint = min(sizehint, self.length - self.bytes_read) + + lines = [] + seen = 0 + while True: + line = self.readline() + if not line: + break + lines.append(line) + seen += len(line) + if seen >= sizehint: + break + return lines + + def finish(self): + self.done = True + if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'): + self.trailers = {} + + try: + for line in self.fp.read_trailer_lines(): + if line[0] in ntob(' \t'): + # It's a continuation line. + v = line.strip() + else: + try: + k, v = line.split(ntob(":"), 1) + except ValueError: + raise ValueError("Illegal header line.") + k = k.strip().title() + v = v.strip() + + if k in comma_separated_headers: + existing = self.trailers.get(envname) + if existing: + v = ntob(", ").join((existing, v)) + self.trailers[k] = v + except Exception: + e = sys.exc_info()[1] + if e.__class__.__name__ == 'MaxSizeExceeded': + # Post data is too big + raise cherrypy.HTTPError( + 413, "Maximum request length: %r" % e.args[1]) + else: + raise + + +class RequestBody(Entity): + """The entity of the HTTP request.""" + + bufsize = 8 * 1024 + """The buffer size used when reading the socket.""" + + # Don't parse the request body at all if the client didn't provide + # a Content-Type header. See http://www.cherrypy.org/ticket/790 + default_content_type = '' + """This defines a default ``Content-Type`` to use if no Content-Type header + is given. The empty string is used for RequestBody, which results in the + request body not being read or parsed at all. This is by design; a missing + ``Content-Type`` header in the HTTP request entity is an error at best, + and a security hole at worst. For multipart parts, however, the MIME spec + declares that a part with no Content-Type defaults to "text/plain" + (see :class:`Part`). + """ + + maxbytes = None + """Raise ``MaxSizeExceeded`` if more bytes than this are read from the socket.""" + + def __init__(self, fp, headers, params=None, request_params=None): + Entity.__init__(self, fp, headers, params) + + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1 + # When no explicit charset parameter is provided by the + # sender, media subtypes of the "text" type are defined + # to have a default charset value of "ISO-8859-1" when + # received via HTTP. + if self.content_type.value.startswith('text/'): + for c in ('ISO-8859-1', 'iso-8859-1', 'Latin-1', 'latin-1'): + if c in self.attempt_charsets: + break + else: + self.attempt_charsets.append('ISO-8859-1') + + # Temporary fix while deprecating passing .parts as .params. + self.processors['multipart'] = _old_process_multipart + + if request_params is None: + request_params = {} + self.request_params = request_params + + def process(self): + """Process the request entity based on its Content-Type.""" + # "The presence of a message-body in a request is signaled by the + # inclusion of a Content-Length or Transfer-Encoding header field in + # the request's message-headers." + # It is possible to send a POST request with no body, for example; + # however, app developers are responsible in that case to set + # cherrypy.request.process_body to False so this method isn't called. + h = cherrypy.serving.request.headers + if 'Content-Length' not in h and 'Transfer-Encoding' not in h: + raise cherrypy.HTTPError(411) + + self.fp = SizedReader(self.fp, self.length, + self.maxbytes, bufsize=self.bufsize, + has_trailers='Trailer' in h) + super(RequestBody, self).process() + + # Body params should also be a part of the request_params + # add them in here. + request_params = self.request_params + for key, value in self.params.items(): + # Python 2 only: keyword arguments must be byte strings (type 'str'). + if sys.version_info < (3, 0): + if isinstance(key, unicode): + key = key.encode('ISO-8859-1') + + if key in request_params: + if not isinstance(request_params[key], list): + request_params[key] = [request_params[key]] + request_params[key].append(value) + else: + request_params[key] = value diff --git a/libs/cherrypy/_cprequest.py b/libs/cherrypy/_cprequest.py new file mode 100644 index 0000000..5890c72 --- /dev/null +++ b/libs/cherrypy/_cprequest.py @@ -0,0 +1,956 @@ + +import os +import sys +import time +import warnings + +import cherrypy +from cherrypy._cpcompat import basestring, copykeys, ntob, unicodestr +from cherrypy._cpcompat import SimpleCookie, CookieError, py3k +from cherrypy import _cpreqbody, _cpconfig +from cherrypy._cperror import format_exc, bare_error +from cherrypy.lib import httputil, file_generator + + +class Hook(object): + """A callback and its metadata: failsafe, priority, and kwargs.""" + + callback = None + """ + The bare callable that this Hook object is wrapping, which will + be called when the Hook is called.""" + + failsafe = False + """ + If True, the callback is guaranteed to run even if other callbacks + from the same call point raise exceptions.""" + + priority = 50 + """ + Defines the order of execution for a list of Hooks. Priority numbers + should be limited to the closed interval [0, 100], but values outside + this range are acceptable, as are fractional values.""" + + kwargs = {} + """ + A set of keyword arguments that will be passed to the + callable on each call.""" + + def __init__(self, callback, failsafe=None, priority=None, **kwargs): + self.callback = callback + + if failsafe is None: + failsafe = getattr(callback, "failsafe", False) + self.failsafe = failsafe + + if priority is None: + priority = getattr(callback, "priority", 50) + self.priority = priority + + self.kwargs = kwargs + + def __lt__(self, other): + # Python 3 + return self.priority < other.priority + + def __cmp__(self, other): + # Python 2 + return cmp(self.priority, other.priority) + + def __call__(self): + """Run self.callback(**self.kwargs).""" + return self.callback(**self.kwargs) + + def __repr__(self): + cls = self.__class__ + return ("%s.%s(callback=%r, failsafe=%r, priority=%r, %s)" + % (cls.__module__, cls.__name__, self.callback, + self.failsafe, self.priority, + ", ".join(['%s=%r' % (k, v) + for k, v in self.kwargs.items()]))) + + +class HookMap(dict): + """A map of call points to lists of callbacks (Hook objects).""" + + def __new__(cls, points=None): + d = dict.__new__(cls) + for p in points or []: + d[p] = [] + return d + + def __init__(self, *a, **kw): + pass + + def attach(self, point, callback, failsafe=None, priority=None, **kwargs): + """Append a new Hook made from the supplied arguments.""" + self[point].append(Hook(callback, failsafe, priority, **kwargs)) + + def run(self, point): + """Execute all registered Hooks (callbacks) for the given point.""" + exc = None + hooks = self[point] + hooks.sort() + for hook in hooks: + # Some hooks are guaranteed to run even if others at + # the same hookpoint fail. We will still log the failure, + # but proceed on to the next hook. The only way + # to stop all processing from one of these hooks is + # to raise SystemExit and stop the whole server. + if exc is None or hook.failsafe: + try: + hook() + except (KeyboardInterrupt, SystemExit): + raise + except (cherrypy.HTTPError, cherrypy.HTTPRedirect, + cherrypy.InternalRedirect): + exc = sys.exc_info()[1] + except: + exc = sys.exc_info()[1] + cherrypy.log(traceback=True, severity=40) + if exc: + raise exc + + def __copy__(self): + newmap = self.__class__() + # We can't just use 'update' because we want copies of the + # mutable values (each is a list) as well. + for k, v in self.items(): + newmap[k] = v[:] + return newmap + copy = __copy__ + + def __repr__(self): + cls = self.__class__ + return "%s.%s(points=%r)" % (cls.__module__, cls.__name__, copykeys(self)) + + +# Config namespace handlers + +def hooks_namespace(k, v): + """Attach bare hooks declared in config.""" + # Use split again to allow multiple hooks for a single + # hookpoint per path (e.g. "hooks.before_handler.1"). + # Little-known fact you only get from reading source ;) + hookpoint = k.split(".", 1)[0] + if isinstance(v, basestring): + v = cherrypy.lib.attributes(v) + if not isinstance(v, Hook): + v = Hook(v) + cherrypy.serving.request.hooks[hookpoint].append(v) + +def request_namespace(k, v): + """Attach request attributes declared in config.""" + # Provides config entries to set request.body attrs (like attempt_charsets). + if k[:5] == 'body.': + setattr(cherrypy.serving.request.body, k[5:], v) + else: + setattr(cherrypy.serving.request, k, v) + +def response_namespace(k, v): + """Attach response attributes declared in config.""" + # Provides config entries to set default response headers + # http://cherrypy.org/ticket/889 + if k[:8] == 'headers.': + cherrypy.serving.response.headers[k.split('.', 1)[1]] = v + else: + setattr(cherrypy.serving.response, k, v) + +def error_page_namespace(k, v): + """Attach error pages declared in config.""" + if k != 'default': + k = int(k) + cherrypy.serving.request.error_page[k] = v + + +hookpoints = ['on_start_resource', 'before_request_body', + 'before_handler', 'before_finalize', + 'on_end_resource', 'on_end_request', + 'before_error_response', 'after_error_response'] + + +class Request(object): + """An HTTP request. + + This object represents the metadata of an HTTP request message; + that is, it contains attributes which describe the environment + in which the request URL, headers, and body were sent (if you + want tools to interpret the headers and body, those are elsewhere, + mostly in Tools). This 'metadata' consists of socket data, + transport characteristics, and the Request-Line. This object + also contains data regarding the configuration in effect for + the given URL, and the execution plan for generating a response. + """ + + prev = None + """ + The previous Request object (if any). This should be None + unless we are processing an InternalRedirect.""" + + # Conversation/connection attributes + local = httputil.Host("127.0.0.1", 80) + "An httputil.Host(ip, port, hostname) object for the server socket." + + remote = httputil.Host("127.0.0.1", 1111) + "An httputil.Host(ip, port, hostname) object for the client socket." + + scheme = "http" + """ + The protocol used between client and server. In most cases, + this will be either 'http' or 'https'.""" + + server_protocol = "HTTP/1.1" + """ + The HTTP version for which the HTTP server is at least + conditionally compliant.""" + + base = "" + """The (scheme://host) portion of the requested URL. + In some cases (e.g. when proxying via mod_rewrite), this may contain + path segments which cherrypy.url uses when constructing url's, but + which otherwise are ignored by CherryPy. Regardless, this value + MUST NOT end in a slash.""" + + # Request-Line attributes + request_line = "" + """ + The complete Request-Line received from the client. This is a + single string consisting of the request method, URI, and protocol + version (joined by spaces). Any final CRLF is removed.""" + + method = "GET" + """ + Indicates the HTTP method to be performed on the resource identified + by the Request-URI. Common methods include GET, HEAD, POST, PUT, and + DELETE. CherryPy allows any extension method; however, various HTTP + servers and gateways may restrict the set of allowable methods. + CherryPy applications SHOULD restrict the set (on a per-URI basis).""" + + query_string = "" + """ + The query component of the Request-URI, a string of information to be + interpreted by the resource. The query portion of a URI follows the + path component, and is separated by a '?'. For example, the URI + 'http://www.cherrypy.org/wiki?a=3&b=4' has the query component, + 'a=3&b=4'.""" + + query_string_encoding = 'utf8' + """ + The encoding expected for query string arguments after % HEX HEX decoding). + If a query string is provided that cannot be decoded with this encoding, + 404 is raised (since technically it's a different URI). If you want + arbitrary encodings to not error, set this to 'Latin-1'; you can then + encode back to bytes and re-decode to whatever encoding you like later. + """ + + protocol = (1, 1) + """The HTTP protocol version corresponding to the set + of features which should be allowed in the response. If BOTH + the client's request message AND the server's level of HTTP + compliance is HTTP/1.1, this attribute will be the tuple (1, 1). + If either is 1.0, this attribute will be the tuple (1, 0). + Lower HTTP protocol versions are not explicitly supported.""" + + params = {} + """ + A dict which combines query string (GET) and request entity (POST) + variables. This is populated in two stages: GET params are added + before the 'on_start_resource' hook, and POST params are added + between the 'before_request_body' and 'before_handler' hooks.""" + + # Message attributes + header_list = [] + """ + A list of the HTTP request headers as (name, value) tuples. + In general, you should use request.headers (a dict) instead.""" + + headers = httputil.HeaderMap() + """ + A dict-like object containing the request headers. Keys are header + names (in Title-Case format); however, you may get and set them in + a case-insensitive manner. That is, headers['Content-Type'] and + headers['content-type'] refer to the same value. Values are header + values (decoded according to :rfc:`2047` if necessary). See also: + httputil.HeaderMap, httputil.HeaderElement.""" + + cookie = SimpleCookie() + """See help(Cookie).""" + + rfile = None + """ + If the request included an entity (body), it will be available + as a stream in this attribute. However, the rfile will normally + be read for you between the 'before_request_body' hook and the + 'before_handler' hook, and the resulting string is placed into + either request.params or the request.body attribute. + + You may disable the automatic consumption of the rfile by setting + request.process_request_body to False, either in config for the desired + path, or in an 'on_start_resource' or 'before_request_body' hook. + + WARNING: In almost every case, you should not attempt to read from the + rfile stream after CherryPy's automatic mechanism has read it. If you + turn off the automatic parsing of rfile, you should read exactly the + number of bytes specified in request.headers['Content-Length']. + Ignoring either of these warnings may result in a hung request thread + or in corruption of the next (pipelined) request. + """ + + process_request_body = True + """ + If True, the rfile (if any) is automatically read and parsed, + and the result placed into request.params or request.body.""" + + methods_with_bodies = ("POST", "PUT") + """ + A sequence of HTTP methods for which CherryPy will automatically + attempt to read a body from the rfile.""" + + body = None + """ + If the request Content-Type is 'application/x-www-form-urlencoded' + or multipart, this will be None. Otherwise, this will be an instance + of :class:`RequestBody` (which you + can .read()); this value is set between the 'before_request_body' and + 'before_handler' hooks (assuming that process_request_body is True).""" + + # Dispatch attributes + dispatch = cherrypy.dispatch.Dispatcher() + """ + The object which looks up the 'page handler' callable and collects + config for the current request based on the path_info, other + request attributes, and the application architecture. The core + calls the dispatcher as early as possible, passing it a 'path_info' + argument. + + The default dispatcher discovers the page handler by matching path_info + to a hierarchical arrangement of objects, starting at request.app.root. + See help(cherrypy.dispatch) for more information.""" + + script_name = "" + """ + The 'mount point' of the application which is handling this request. + + This attribute MUST NOT end in a slash. If the script_name refers to + the root of the URI, it MUST be an empty string (not "/"). + """ + + path_info = "/" + """ + The 'relative path' portion of the Request-URI. This is relative + to the script_name ('mount point') of the application which is + handling this request.""" + + login = None + """ + When authentication is used during the request processing this is + set to 'False' if it failed and to the 'username' value if it succeeded. + The default 'None' implies that no authentication happened.""" + + # Note that cherrypy.url uses "if request.app:" to determine whether + # the call is during a real HTTP request or not. So leave this None. + app = None + """The cherrypy.Application object which is handling this request.""" + + handler = None + """ + The function, method, or other callable which CherryPy will call to + produce the response. The discovery of the handler and the arguments + it will receive are determined by the request.dispatch object. + By default, the handler is discovered by walking a tree of objects + starting at request.app.root, and is then passed all HTTP params + (from the query string and POST body) as keyword arguments.""" + + toolmaps = {} + """ + A nested dict of all Toolboxes and Tools in effect for this request, + of the form: {Toolbox.namespace: {Tool.name: config dict}}.""" + + config = None + """ + A flat dict of all configuration entries which apply to the + current request. These entries are collected from global config, + application config (based on request.path_info), and from handler + config (exactly how is governed by the request.dispatch object in + effect for this request; by default, handler config can be attached + anywhere in the tree between request.app.root and the final handler, + and inherits downward).""" + + is_index = None + """ + This will be True if the current request is mapped to an 'index' + resource handler (also, a 'default' handler if path_info ends with + a slash). The value may be used to automatically redirect the + user-agent to a 'more canonical' URL which either adds or removes + the trailing slash. See cherrypy.tools.trailing_slash.""" + + hooks = HookMap(hookpoints) + """ + A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}. + Each key is a str naming the hook point, and each value is a list + of hooks which will be called at that hook point during this request. + The list of hooks is generally populated as early as possible (mostly + from Tools specified in config), but may be extended at any time. + See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools.""" + + error_response = cherrypy.HTTPError(500).set_response + """ + The no-arg callable which will handle unexpected, untrapped errors + during request processing. This is not used for expected exceptions + (like NotFound, HTTPError, or HTTPRedirect) which are raised in + response to expected conditions (those should be customized either + via request.error_page or by overriding HTTPError.set_response). + By default, error_response uses HTTPError(500) to return a generic + error response to the user-agent.""" + + error_page = {} + """ + A dict of {error code: response filename or callable} pairs. + + The error code must be an int representing a given HTTP error code, + or the string 'default', which will be used if no matching entry + is found for a given numeric code. + + If a filename is provided, the file should contain a Python string- + formatting template, and can expect by default to receive format + values with the mapping keys %(status)s, %(message)s, %(traceback)s, + and %(version)s. The set of format mappings can be extended by + overriding HTTPError.set_response. + + If a callable is provided, it will be called by default with keyword + arguments 'status', 'message', 'traceback', and 'version', as for a + string-formatting template. The callable must return a string or iterable of + strings which will be set to response.body. It may also override headers or + perform any other processing. + + If no entry is given for an error code, and no 'default' entry exists, + a default template will be used. + """ + + show_tracebacks = True + """ + If True, unexpected errors encountered during request processing will + include a traceback in the response body.""" + + show_mismatched_params = True + """ + If True, mismatched parameters encountered during PageHandler invocation + processing will be included in the response body.""" + + throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect) + """The sequence of exceptions which Request.run does not trap.""" + + throw_errors = False + """ + If True, Request.run will not trap any errors (except HTTPRedirect and + HTTPError, which are more properly called 'exceptions', not errors).""" + + closed = False + """True once the close method has been called, False otherwise.""" + + stage = None + """ + A string containing the stage reached in the request-handling process. + This is useful when debugging a live server with hung requests.""" + + namespaces = _cpconfig.NamespaceSet( + **{"hooks": hooks_namespace, + "request": request_namespace, + "response": response_namespace, + "error_page": error_page_namespace, + "tools": cherrypy.tools, + }) + + def __init__(self, local_host, remote_host, scheme="http", + server_protocol="HTTP/1.1"): + """Populate a new Request object. + + local_host should be an httputil.Host object with the server info. + remote_host should be an httputil.Host object with the client info. + scheme should be a string, either "http" or "https". + """ + self.local = local_host + self.remote = remote_host + self.scheme = scheme + self.server_protocol = server_protocol + + self.closed = False + + # Put a *copy* of the class error_page into self. + self.error_page = self.error_page.copy() + + # Put a *copy* of the class namespaces into self. + self.namespaces = self.namespaces.copy() + + self.stage = None + + def close(self): + """Run cleanup code. (Core)""" + if not self.closed: + self.closed = True + self.stage = 'on_end_request' + self.hooks.run('on_end_request') + self.stage = 'close' + + def run(self, method, path, query_string, req_protocol, headers, rfile): + r"""Process the Request. (Core) + + method, path, query_string, and req_protocol should be pulled directly + from the Request-Line (e.g. "GET /path?key=val HTTP/1.0"). + + path + This should be %XX-unquoted, but query_string should not be. + + When using Python 2, they both MUST be byte strings, + not unicode strings. + + When using Python 3, they both MUST be unicode strings, + not byte strings, and preferably not bytes \x00-\xFF + disguised as unicode. + + headers + A list of (name, value) tuples. + + rfile + A file-like object containing the HTTP request entity. + + When run() is done, the returned object should have 3 attributes: + + * status, e.g. "200 OK" + * header_list, a list of (name, value) tuples + * body, an iterable yielding strings + + Consumer code (HTTP servers) should then access these response + attributes to build the outbound stream. + + """ + response = cherrypy.serving.response + self.stage = 'run' + try: + self.error_response = cherrypy.HTTPError(500).set_response + + self.method = method + path = path or "/" + self.query_string = query_string or '' + self.params = {} + + # 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(self.server_protocol[5]), int(self.server_protocol[7]) + self.protocol = min(rp, sp) + response.headers.protocol = self.protocol + + # Rebuild first line of the request (e.g. "GET /path HTTP/1.0"). + url = path + if query_string: + url += '?' + query_string + self.request_line = '%s %s %s' % (method, url, req_protocol) + + self.header_list = list(headers) + self.headers = httputil.HeaderMap() + + self.rfile = rfile + self.body = None + + self.cookie = SimpleCookie() + self.handler = None + + # path_info should be the path from the + # app root (script_name) to the handler. + self.script_name = self.app.script_name + self.path_info = pi = path[len(self.script_name):] + + self.stage = 'respond' + self.respond(pi) + + except self.throws: + raise + except: + if self.throw_errors: + raise + else: + # Failure in setup, error handler or finalize. Bypass them. + # Can't use handle_error because we may not have hooks yet. + cherrypy.log(traceback=True, severity=40) + if self.show_tracebacks: + body = format_exc() + else: + body = "" + r = bare_error(body) + response.output_status, response.header_list, response.body = r + + if self.method == "HEAD": + # HEAD requests MUST NOT return a message-body in the response. + response.body = [] + + try: + cherrypy.log.access() + except: + cherrypy.log.error(traceback=True) + + if response.timed_out: + raise cherrypy.TimeoutError() + + return response + + # Uncomment for stage debugging + # stage = property(lambda self: self._stage, lambda self, v: print(v)) + + def respond(self, path_info): + """Generate a response for the resource at self.path_info. (Core)""" + response = cherrypy.serving.response + try: + try: + try: + if self.app is None: + raise cherrypy.NotFound() + + # Get the 'Host' header, so we can HTTPRedirect properly. + self.stage = 'process_headers' + self.process_headers() + + # Make a copy of the class hooks + self.hooks = self.__class__.hooks.copy() + self.toolmaps = {} + + self.stage = 'get_resource' + self.get_resource(path_info) + + self.body = _cpreqbody.RequestBody( + self.rfile, self.headers, request_params=self.params) + + self.namespaces(self.config) + + self.stage = 'on_start_resource' + self.hooks.run('on_start_resource') + + # Parse the querystring + self.stage = 'process_query_string' + self.process_query_string() + + # Process the body + if self.process_request_body: + if self.method not in self.methods_with_bodies: + self.process_request_body = False + self.stage = 'before_request_body' + self.hooks.run('before_request_body') + if self.process_request_body: + self.body.process() + + # Run the handler + self.stage = 'before_handler' + self.hooks.run('before_handler') + if self.handler: + self.stage = 'handler' + response.body = self.handler() + + # Finalize + self.stage = 'before_finalize' + self.hooks.run('before_finalize') + response.finalize() + except (cherrypy.HTTPRedirect, cherrypy.HTTPError): + inst = sys.exc_info()[1] + inst.set_response() + self.stage = 'before_finalize (HTTPError)' + self.hooks.run('before_finalize') + response.finalize() + finally: + self.stage = 'on_end_resource' + self.hooks.run('on_end_resource') + except self.throws: + raise + except: + if self.throw_errors: + raise + self.handle_error() + + def process_query_string(self): + """Parse the query string into Python structures. (Core)""" + try: + p = httputil.parse_query_string( + self.query_string, encoding=self.query_string_encoding) + except UnicodeDecodeError: + raise cherrypy.HTTPError( + 404, "The given query string could not be processed. Query " + "strings for this resource must be encoded with %r." % + self.query_string_encoding) + + # Python 2 only: keyword arguments must be byte strings (type 'str'). + if not py3k: + for key, value in p.items(): + if isinstance(key, unicode): + del p[key] + p[key.encode(self.query_string_encoding)] = value + self.params.update(p) + + def process_headers(self): + """Parse HTTP header data into Python structures. (Core)""" + # Process the headers into self.headers + headers = self.headers + for name, value in self.header_list: + # Call title() now (and use dict.__method__(headers)) + # so title doesn't have to be called twice. + name = name.title() + value = value.strip() + + # Warning: if there is more than one header entry for cookies (AFAIK, + # only Konqueror does that), only the last one will remain in headers + # (but they will be correctly stored in request.cookie). + if "=?" in value: + dict.__setitem__(headers, name, httputil.decode_TEXT(value)) + else: + dict.__setitem__(headers, name, value) + + # Handle cookies differently because on Konqueror, multiple + # cookies come on different lines with the same key + if name == 'Cookie': + try: + self.cookie.load(value) + except CookieError: + msg = "Illegal cookie name %s" % value.split('=')[0] + raise cherrypy.HTTPError(400, msg) + + if not dict.__contains__(headers, 'Host'): + # All Internet-based HTTP/1.1 servers MUST respond with a 400 + # (Bad Request) status code to any HTTP/1.1 request message + # which lacks a Host header field. + if self.protocol >= (1, 1): + msg = "HTTP/1.1 requires a 'Host' request header." + raise cherrypy.HTTPError(400, msg) + host = dict.get(headers, 'Host') + if not host: + host = self.local.name or self.local.ip + self.base = "%s://%s" % (self.scheme, host) + + def get_resource(self, path): + """Call a dispatcher (which sets self.handler and .config). (Core)""" + # First, see if there is a custom dispatch at this URI. Custom + # dispatchers can only be specified in app.config, not in _cp_config + # (since custom dispatchers may not even have an app.root). + dispatch = self.app.find_config(path, "request.dispatch", self.dispatch) + + # dispatch() should set self.handler and self.config + dispatch(path) + + def handle_error(self): + """Handle the last unanticipated exception. (Core)""" + try: + self.hooks.run("before_error_response") + if self.error_response: + self.error_response() + self.hooks.run("after_error_response") + cherrypy.serving.response.finalize() + except cherrypy.HTTPRedirect: + inst = sys.exc_info()[1] + inst.set_response() + cherrypy.serving.response.finalize() + + # ------------------------- Properties ------------------------- # + + def _get_body_params(self): + warnings.warn( + "body_params is deprecated in CherryPy 3.2, will be removed in " + "CherryPy 3.3.", + DeprecationWarning + ) + return self.body.params + body_params = property(_get_body_params, + doc= """ + If the request Content-Type is 'application/x-www-form-urlencoded' or + multipart, this will be a dict of the params pulled from the entity + body; that is, it will be the portion of request.params that come + from the message body (sometimes called "POST params", although they + can be sent with various HTTP method verbs). This value is set between + the 'before_request_body' and 'before_handler' hooks (assuming that + process_request_body is True). + + Deprecated in 3.2, will be removed for 3.3 in favor of + :attr:`request.body.params`.""") + + +class ResponseBody(object): + """The body of the HTTP response (the response entity).""" + + if py3k: + unicode_err = ("Page handlers MUST return bytes. Use tools.encode " + "if you wish to return unicode.") + + def __get__(self, obj, objclass=None): + if obj is None: + # When calling on the class instead of an instance... + return self + else: + return obj._body + + def __set__(self, obj, value): + # Convert the given value to an iterable object. + if py3k and isinstance(value, str): + raise ValueError(self.unicode_err) + + if isinstance(value, basestring): + # strings get wrapped in a list because iterating over a single + # item list is much faster than iterating over every character + # in a long string. + if value: + value = [value] + else: + # [''] doesn't evaluate to False, so replace it with []. + value = [] + elif py3k and isinstance(value, list): + # every item in a list must be bytes... + for i, item in enumerate(value): + if isinstance(item, str): + raise ValueError(self.unicode_err) + # Don't use isinstance here; io.IOBase which has an ABC takes + # 1000 times as long as, say, isinstance(value, str) + elif hasattr(value, 'read'): + value = file_generator(value) + elif value is None: + value = [] + obj._body = value + + +class Response(object): + """An HTTP Response, including status, headers, and body.""" + + status = "" + """The HTTP Status-Code and Reason-Phrase.""" + + header_list = [] + """ + A list of the HTTP response headers as (name, value) tuples. + In general, you should use response.headers (a dict) instead. This + attribute is generated from response.headers and is not valid until + after the finalize phase.""" + + headers = httputil.HeaderMap() + """ + A dict-like object containing the response headers. Keys are header + names (in Title-Case format); however, you may get and set them in + a case-insensitive manner. That is, headers['Content-Type'] and + headers['content-type'] refer to the same value. Values are header + values (decoded according to :rfc:`2047` if necessary). + + .. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement` + """ + + cookie = SimpleCookie() + """See help(Cookie).""" + + body = ResponseBody() + """The body (entity) of the HTTP response.""" + + time = None + """The value of time.time() when created. Use in HTTP dates.""" + + timeout = 300 + """Seconds after which the response will be aborted.""" + + timed_out = False + """ + Flag to indicate the response should be aborted, because it has + exceeded its timeout.""" + + stream = False + """If False, buffer the response body.""" + + def __init__(self): + self.status = None + self.header_list = None + self._body = [] + self.time = time.time() + + self.headers = httputil.HeaderMap() + # Since we know all our keys are titled strings, we can + # bypass HeaderMap.update and get a big speed boost. + dict.update(self.headers, { + "Content-Type": 'text/html', + "Server": "CherryPy/" + cherrypy.__version__, + "Date": httputil.HTTPDate(self.time), + }) + self.cookie = SimpleCookie() + + def collapse_body(self): + """Collapse self.body to a single string; replace it and return it.""" + if isinstance(self.body, basestring): + return self.body + + newbody = [] + for chunk in self.body: + if py3k and not isinstance(chunk, bytes): + raise TypeError("Chunk %s is not of type 'bytes'." % repr(chunk)) + newbody.append(chunk) + newbody = ntob('').join(newbody) + + self.body = newbody + return newbody + + def finalize(self): + """Transform headers (and cookies) into self.header_list. (Core)""" + try: + code, reason, _ = httputil.valid_status(self.status) + except ValueError: + raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0]) + + headers = self.headers + + self.status = "%s %s" % (code, reason) + self.output_status = ntob(str(code), 'ascii') + ntob(" ") + headers.encode(reason) + + if self.stream: + # The upshot: wsgiserver will chunk the response if + # you pop Content-Length (or set it explicitly to None). + # Note that lib.static sets C-L to the file's st_size. + if dict.get(headers, 'Content-Length') is None: + dict.pop(headers, 'Content-Length', None) + elif code < 200 or code in (204, 205, 304): + # "All 1xx (informational), 204 (no content), + # and 304 (not modified) responses MUST NOT + # include a message-body." + dict.pop(headers, 'Content-Length', None) + self.body = ntob("") + else: + # Responses which are not streamed should have a Content-Length, + # but allow user code to set Content-Length if desired. + if dict.get(headers, 'Content-Length') is None: + content = self.collapse_body() + dict.__setitem__(headers, 'Content-Length', len(content)) + + # Transform our header dict into a list of tuples. + self.header_list = h = headers.output() + + cookie = self.cookie.output() + if cookie: + for line in cookie.split("\n"): + if line.endswith("\r"): + # Python 2.4 emits cookies joined by LF but 2.5+ by CRLF. + line = line[:-1] + name, value = line.split(": ", 1) + if isinstance(name, unicodestr): + name = name.encode("ISO-8859-1") + if isinstance(value, unicodestr): + value = headers.encode(value) + h.append((name, value)) + + def check_timeout(self): + """If now > self.time + self.timeout, set self.timed_out. + + This purposefully sets a flag, rather than raising an error, + so that a monitor thread can interrupt the Response thread. + """ + if time.time() > self.time + self.timeout: + self.timed_out = True + + + diff --git a/libs/cherrypy/_cpserver.py b/libs/cherrypy/_cpserver.py new file mode 100644 index 0000000..2eecd6e --- /dev/null +++ b/libs/cherrypy/_cpserver.py @@ -0,0 +1,205 @@ +"""Manage HTTP servers with CherryPy.""" + +import warnings + +import cherrypy +from cherrypy.lib import attributes +from cherrypy._cpcompat import basestring, py3k + +# We import * because we want to export check_port +# et al as attributes of this module. +from cherrypy.process.servers import * + + +class Server(ServerAdapter): + """An adapter for an HTTP server. + + You can set attributes (like socket_host and socket_port) + on *this* object (which is probably cherrypy.server), and call + quickstart. For example:: + + cherrypy.server.socket_port = 80 + cherrypy.quickstart() + """ + + socket_port = 8080 + """The TCP port on which to listen for connections.""" + + _socket_host = '127.0.0.1' + def _get_socket_host(self): + return self._socket_host + def _set_socket_host(self, value): + if value == '': + raise ValueError("The empty string ('') is not an allowed value. " + "Use '0.0.0.0' instead to listen on all active " + "interfaces (INADDR_ANY).") + self._socket_host = value + socket_host = property(_get_socket_host, _set_socket_host, + doc="""The hostname or IP address on which to listen for connections. + + Host values may be any IPv4 or IPv6 address, or any valid hostname. + The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if + your hosts file prefers IPv6). The string '0.0.0.0' is a special + IPv4 entry meaning "any active interface" (INADDR_ANY), and '::' + is the similar IN6ADDR_ANY for IPv6. The empty string or None are + not allowed.""") + + socket_file = None + """If given, the name of the UNIX socket to use instead of TCP/IP. + + When this option is not None, the `socket_host` and `socket_port` options + are ignored.""" + + socket_queue_size = 5 + """The 'backlog' argument to socket.listen(); specifies the maximum number + of queued connections (default 5).""" + + socket_timeout = 10 + """The timeout in seconds for accepted connections (default 10).""" + + shutdown_timeout = 5 + """The time to wait for HTTP worker threads to clean up.""" + + protocol_version = 'HTTP/1.1' + """The version string to write in the Status-Line of all HTTP responses, + for example, "HTTP/1.1" (the default). Depending on the HTTP server used, + this should also limit the supported features used in the response.""" + + thread_pool = 10 + """The number of worker threads to start up in the pool.""" + + thread_pool_max = -1 + """The maximum size of the worker-thread pool. Use -1 to indicate no limit.""" + + max_request_header_size = 500 * 1024 + """The maximum number of bytes allowable in the request headers. If exceeded, + the HTTP server should return "413 Request Entity Too Large".""" + + max_request_body_size = 100 * 1024 * 1024 + """The maximum number of bytes allowable in the request body. If exceeded, + the HTTP server should return "413 Request Entity Too Large".""" + + instance = None + """If not None, this should be an HTTP server instance (such as + CPWSGIServer) which cherrypy.server will control. Use this when you need + more control over object instantiation than is available in the various + configuration options.""" + + ssl_context = None + """When using PyOpenSSL, an instance of SSL.Context.""" + + ssl_certificate = None + """The filename of the SSL certificate to use.""" + + ssl_certificate_chain = None + """When using PyOpenSSL, the certificate chain to pass to + Context.load_verify_locations.""" + + ssl_private_key = None + """The filename of the private key to use with SSL.""" + + if py3k: + ssl_module = 'builtin' + """The name of a registered SSL adaptation module to use with the builtin + WSGI server. Builtin options are: 'builtin' (to use the SSL library built + into recent versions of Python). You may also register your + own classes in the wsgiserver.ssl_adapters dict.""" + else: + ssl_module = 'pyopenssl' + """The name of a registered SSL adaptation module to use with the builtin + WSGI server. Builtin options are 'builtin' (to use the SSL library built + into recent versions of Python) and 'pyopenssl' (to use the PyOpenSSL + project, which you must install separately). You may also register your + own classes in the wsgiserver.ssl_adapters dict.""" + + statistics = False + """Turns statistics-gathering on or off for aware HTTP servers.""" + + nodelay = True + """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" + + wsgi_version = (1, 0) + """The WSGI version tuple to use with the builtin WSGI server. + The provided options are (1, 0) [which includes support for PEP 3333, + which declares it covers WSGI version 1.0.1 but still mandates the + wsgi.version (1, 0)] and ('u', 0), an experimental unicode version. + You may create and register your own experimental versions of the WSGI + protocol by adding custom classes to the wsgiserver.wsgi_gateways dict.""" + + def __init__(self): + self.bus = cherrypy.engine + self.httpserver = None + self.interrupt = None + self.running = False + + def httpserver_from_self(self, httpserver=None): + """Return a (httpserver, bind_addr) pair based on self attributes.""" + if httpserver is None: + httpserver = self.instance + if httpserver is None: + from cherrypy import _cpwsgi_server + httpserver = _cpwsgi_server.CPWSGIServer(self) + if isinstance(httpserver, basestring): + # Is anyone using this? Can I add an arg? + httpserver = attributes(httpserver)(self) + return httpserver, self.bind_addr + + def start(self): + """Start the HTTP server.""" + if not self.httpserver: + self.httpserver, self.bind_addr = self.httpserver_from_self() + ServerAdapter.start(self) + start.priority = 75 + + def _get_bind_addr(self): + if self.socket_file: + return self.socket_file + if self.socket_host is None and self.socket_port is None: + return None + return (self.socket_host, self.socket_port) + def _set_bind_addr(self, value): + if value is None: + self.socket_file = None + self.socket_host = None + self.socket_port = None + elif isinstance(value, basestring): + self.socket_file = value + self.socket_host = None + self.socket_port = None + else: + try: + self.socket_host, self.socket_port = value + self.socket_file = None + except ValueError: + raise ValueError("bind_addr must be a (host, port) tuple " + "(for TCP sockets) or a string (for Unix " + "domain sockets), not %r" % value) + bind_addr = property(_get_bind_addr, _set_bind_addr, + doc='A (host, port) tuple for TCP sockets or a str for Unix domain sockets.') + + def base(self): + """Return the base (scheme://host[:port] or sock file) for this server.""" + if self.socket_file: + return self.socket_file + + host = self.socket_host + if host in ('0.0.0.0', '::'): + # 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY. + # Look up the host name, which should be the + # safest thing to spit out in a URL. + import socket + host = socket.gethostname() + + port = self.socket_port + + if self.ssl_certificate: + scheme = "https" + if port != 443: + host += ":%s" % port + else: + scheme = "http" + if port != 80: + host += ":%s" % port + + return "%s://%s" % (scheme, host) + diff --git a/libs/cherrypy/_cpthreadinglocal.py b/libs/cherrypy/_cpthreadinglocal.py new file mode 100644 index 0000000..34c17ac --- /dev/null +++ b/libs/cherrypy/_cpthreadinglocal.py @@ -0,0 +1,239 @@ +# This is a backport of Python-2.4's threading.local() implementation + +"""Thread-local objects + +(Note that this module provides a Python version of thread + threading.local class. Depending on the version of Python you're + using, there may be a faster one available. You should always import + the local class from threading.) + +Thread-local objects support the management of thread-local data. +If you have data that you want to be local to a thread, simply create +a thread-local object and use its attributes: + + >>> mydata = local() + >>> mydata.number = 42 + >>> mydata.number + 42 + +You can also access the local-object's dictionary: + + >>> mydata.__dict__ + {'number': 42} + >>> mydata.__dict__.setdefault('widgets', []) + [] + >>> mydata.widgets + [] + +What's important about thread-local objects is that their data are +local to a thread. If we access the data in a different thread: + + >>> log = [] + >>> def f(): + ... items = mydata.__dict__.items() + ... items.sort() + ... log.append(items) + ... mydata.number = 11 + ... log.append(mydata.number) + + >>> import threading + >>> thread = threading.Thread(target=f) + >>> thread.start() + >>> thread.join() + >>> log + [[], 11] + +we get different data. Furthermore, changes made in the other thread +don't affect data seen in this thread: + + >>> mydata.number + 42 + +Of course, values you get from a local object, including a __dict__ +attribute, are for whatever thread was current at the time the +attribute was read. For that reason, you generally don't want to save +these values across threads, as they apply only to the thread they +came from. + +You can create custom local objects by subclassing the local class: + + >>> class MyLocal(local): + ... number = 2 + ... initialized = False + ... def __init__(self, **kw): + ... if self.initialized: + ... raise SystemError('__init__ called too many times') + ... self.initialized = True + ... self.__dict__.update(kw) + ... def squared(self): + ... return self.number ** 2 + +This can be useful to support default values, methods and +initialization. Note that if you define an __init__ method, it will be +called each time the local object is used in a separate thread. This +is necessary to initialize each thread's dictionary. + +Now if we create a local object: + + >>> mydata = MyLocal(color='red') + +Now we have a default number: + + >>> mydata.number + 2 + +an initial color: + + >>> mydata.color + 'red' + >>> del mydata.color + +And a method that operates on the data: + + >>> mydata.squared() + 4 + +As before, we can access the data in a separate thread: + + >>> log = [] + >>> thread = threading.Thread(target=f) + >>> thread.start() + >>> thread.join() + >>> log + [[('color', 'red'), ('initialized', True)], 11] + +without affecting this thread's data: + + >>> mydata.number + 2 + >>> mydata.color + Traceback (most recent call last): + ... + AttributeError: 'MyLocal' object has no attribute 'color' + +Note that subclasses can define slots, but they are not thread +local. They are shared across threads: + + >>> class MyLocal(local): + ... __slots__ = 'number' + + >>> mydata = MyLocal() + >>> mydata.number = 42 + >>> mydata.color = 'red' + +So, the separate thread: + + >>> thread = threading.Thread(target=f) + >>> thread.start() + >>> thread.join() + +affects what we see: + + >>> mydata.number + 11 + +>>> del mydata +""" + +# Threading import is at end + +class _localbase(object): + __slots__ = '_local__key', '_local__args', '_local__lock' + + def __new__(cls, *args, **kw): + self = object.__new__(cls) + key = 'thread.local.' + str(id(self)) + object.__setattr__(self, '_local__key', key) + object.__setattr__(self, '_local__args', (args, kw)) + object.__setattr__(self, '_local__lock', RLock()) + + if args or kw and (cls.__init__ is object.__init__): + raise TypeError("Initialization arguments are not supported") + + # We need to create the thread dict in anticipation of + # __init__ being called, to make sure we don't call it + # again ourselves. + dict = object.__getattribute__(self, '__dict__') + currentThread().__dict__[key] = dict + + return self + +def _patch(self): + key = object.__getattribute__(self, '_local__key') + d = currentThread().__dict__.get(key) + if d is None: + d = {} + currentThread().__dict__[key] = d + object.__setattr__(self, '__dict__', d) + + # we have a new instance dict, so call out __init__ if we have + # one + cls = type(self) + if cls.__init__ is not object.__init__: + args, kw = object.__getattribute__(self, '_local__args') + cls.__init__(self, *args, **kw) + else: + object.__setattr__(self, '__dict__', d) + +class local(_localbase): + + def __getattribute__(self, name): + lock = object.__getattribute__(self, '_local__lock') + lock.acquire() + try: + _patch(self) + return object.__getattribute__(self, name) + finally: + lock.release() + + def __setattr__(self, name, value): + lock = object.__getattribute__(self, '_local__lock') + lock.acquire() + try: + _patch(self) + return object.__setattr__(self, name, value) + finally: + lock.release() + + def __delattr__(self, name): + lock = object.__getattribute__(self, '_local__lock') + lock.acquire() + try: + _patch(self) + return object.__delattr__(self, name) + finally: + lock.release() + + + def __del__(): + threading_enumerate = enumerate + __getattribute__ = object.__getattribute__ + + def __del__(self): + key = __getattribute__(self, '_local__key') + + try: + threads = list(threading_enumerate()) + except: + # if enumerate fails, as it seems to do during + # shutdown, we'll skip cleanup under the assumption + # that there is nothing to clean up + return + + for thread in threads: + try: + __dict__ = thread.__dict__ + except AttributeError: + # Thread is dying, rest in peace + continue + + if key in __dict__: + try: + del __dict__[key] + except KeyError: + pass # didn't have anything in this thread + + return __del__ + __del__ = __del__() + +from threading import currentThread, enumerate, RLock diff --git a/libs/cherrypy/_cptools.py b/libs/cherrypy/_cptools.py new file mode 100644 index 0000000..22316b3 --- /dev/null +++ b/libs/cherrypy/_cptools.py @@ -0,0 +1,510 @@ +"""CherryPy tools. A "tool" is any helper, adapted to CP. + +Tools are usually designed to be used in a variety of ways (although some +may only offer one if they choose): + + Library calls + All tools are callables that can be used wherever needed. + The arguments are straightforward and should be detailed within the + docstring. + + Function decorators + All tools, when called, may be used as decorators which configure + individual CherryPy page handlers (methods on the CherryPy tree). + That is, "@tools.anytool()" should "turn on" the tool via the + decorated function's _cp_config attribute. + + CherryPy config + If a tool exposes a "_setup" callable, it will be called + once per Request (if the feature is "turned on" via config). + +Tools may be implemented as any object with a namespace. The builtins +are generally either modules or instances of the tools.Tool class. +""" + +import sys +import warnings + +import cherrypy + + +def _getargs(func): + """Return the names of all static arguments to the given function.""" + # Use this instead of importing inspect for less mem overhead. + import types + if sys.version_info >= (3, 0): + if isinstance(func, types.MethodType): + func = func.__func__ + co = func.__code__ + else: + if isinstance(func, types.MethodType): + func = func.im_func + co = func.func_code + return co.co_varnames[:co.co_argcount] + + +_attr_error = ("CherryPy Tools cannot be turned on directly. Instead, turn them " + "on via config, or use them as decorators on your page handlers.") + +class Tool(object): + """A registered function for use with CherryPy request-processing hooks. + + help(tool.callable) should give you more information about this Tool. + """ + + namespace = "tools" + + def __init__(self, point, callable, name=None, priority=50): + self._point = point + self.callable = callable + self._name = name + self._priority = priority + self.__doc__ = self.callable.__doc__ + self._setargs() + + def _get_on(self): + raise AttributeError(_attr_error) + def _set_on(self, value): + raise AttributeError(_attr_error) + on = property(_get_on, _set_on) + + def _setargs(self): + """Copy func parameter names to obj attributes.""" + try: + for arg in _getargs(self.callable): + setattr(self, arg, None) + except (TypeError, AttributeError): + if hasattr(self.callable, "__call__"): + for arg in _getargs(self.callable.__call__): + setattr(self, arg, None) + # IronPython 1.0 raises NotImplementedError because + # inspect.getargspec tries to access Python bytecode + # in co_code attribute. + except NotImplementedError: + pass + # IronPython 1B1 may raise IndexError in some cases, + # but if we trap it here it doesn't prevent CP from working. + except IndexError: + pass + + def _merged_args(self, d=None): + """Return a dict of configuration entries for this Tool.""" + if d: + conf = d.copy() + else: + conf = {} + + tm = cherrypy.serving.request.toolmaps[self.namespace] + if self._name in tm: + conf.update(tm[self._name]) + + if "on" in conf: + del conf["on"] + + return conf + + def __call__(self, *args, **kwargs): + """Compile-time decorator (turn on the tool in config). + + For example:: + + @tools.proxy() + def whats_my_base(self): + return cherrypy.request.base + whats_my_base.exposed = True + """ + if args: + raise TypeError("The %r Tool does not accept positional " + "arguments; you must use keyword arguments." + % self._name) + def tool_decorator(f): + if not hasattr(f, "_cp_config"): + f._cp_config = {} + subspace = self.namespace + "." + self._name + "." + f._cp_config[subspace + "on"] = True + for k, v in kwargs.items(): + f._cp_config[subspace + k] = v + return f + return tool_decorator + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + conf = self._merged_args() + p = conf.pop("priority", None) + if p is None: + p = getattr(self.callable, "priority", self._priority) + cherrypy.serving.request.hooks.attach(self._point, self.callable, + priority=p, **conf) + + +class HandlerTool(Tool): + """Tool which is called 'before main', that may skip normal handlers. + + If the tool successfully handles the request (by setting response.body), + if should return True. This will cause CherryPy to skip any 'normal' page + handler. If the tool did not handle the request, it should return False + to tell CherryPy to continue on and call the normal page handler. If the + tool is declared AS a page handler (see the 'handler' method), returning + False will raise NotFound. + """ + + def __init__(self, callable, name=None): + Tool.__init__(self, 'before_handler', callable, name) + + def handler(self, *args, **kwargs): + """Use this tool as a CherryPy page handler. + + For example:: + + class Root: + nav = tools.staticdir.handler(section="/nav", dir="nav", + root=absDir) + """ + def handle_func(*a, **kw): + handled = self.callable(*args, **self._merged_args(kwargs)) + if not handled: + raise cherrypy.NotFound() + return cherrypy.serving.response.body + handle_func.exposed = True + return handle_func + + def _wrapper(self, **kwargs): + if self.callable(**kwargs): + cherrypy.serving.request.handler = None + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + conf = self._merged_args() + p = conf.pop("priority", None) + if p is None: + p = getattr(self.callable, "priority", self._priority) + cherrypy.serving.request.hooks.attach(self._point, self._wrapper, + priority=p, **conf) + + +class HandlerWrapperTool(Tool): + """Tool which wraps request.handler in a provided wrapper function. + + The 'newhandler' arg must be a handler wrapper function that takes a + 'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all + page handler + functions, it must return an iterable for use as cherrypy.response.body. + + For example, to allow your 'inner' page handlers to return dicts + which then get interpolated into a template:: + + def interpolator(next_handler, *args, **kwargs): + filename = cherrypy.request.config.get('template') + cherrypy.response.template = env.get_template(filename) + response_dict = next_handler(*args, **kwargs) + return cherrypy.response.template.render(**response_dict) + cherrypy.tools.jinja = HandlerWrapperTool(interpolator) + """ + + def __init__(self, newhandler, point='before_handler', name=None, priority=50): + self.newhandler = newhandler + self._point = point + self._name = name + self._priority = priority + + def callable(self, debug=False): + innerfunc = cherrypy.serving.request.handler + def wrap(*args, **kwargs): + return self.newhandler(innerfunc, *args, **kwargs) + cherrypy.serving.request.handler = wrap + + +class ErrorTool(Tool): + """Tool which is used to replace the default request.error_response.""" + + def __init__(self, callable, name=None): + Tool.__init__(self, None, callable, name) + + def _wrapper(self): + self.callable(**self._merged_args()) + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + cherrypy.serving.request.error_response = self._wrapper + + +# Builtin tools # + +from cherrypy.lib import cptools, encoding, auth, static, jsontools +from cherrypy.lib import sessions as _sessions, xmlrpcutil as _xmlrpc +from cherrypy.lib import caching as _caching +from cherrypy.lib import auth_basic, auth_digest + + +class SessionTool(Tool): + """Session Tool for CherryPy. + + sessions.locking + When 'implicit' (the default), the session will be locked for you, + just before running the page handler. + + When 'early', the session will be locked before reading the request + body. This is off by default for safety reasons; for example, + a large upload would block the session, denying an AJAX + progress meter (see http://www.cherrypy.org/ticket/630). + + When 'explicit' (or any other value), you need to call + cherrypy.session.acquire_lock() yourself before using + session data. + """ + + def __init__(self): + # _sessions.init must be bound after headers are read + Tool.__init__(self, 'before_request_body', _sessions.init) + + def _lock_session(self): + cherrypy.serving.session.acquire_lock() + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + hooks = cherrypy.serving.request.hooks + + conf = self._merged_args() + + p = conf.pop("priority", None) + if p is None: + p = getattr(self.callable, "priority", self._priority) + + hooks.attach(self._point, self.callable, priority=p, **conf) + + locking = conf.pop('locking', 'implicit') + if locking == 'implicit': + hooks.attach('before_handler', self._lock_session) + elif locking == 'early': + # Lock before the request body (but after _sessions.init runs!) + hooks.attach('before_request_body', self._lock_session, + priority=60) + else: + # Don't lock + pass + + hooks.attach('before_finalize', _sessions.save) + hooks.attach('on_end_request', _sessions.close) + + def regenerate(self): + """Drop the current session and make a new one (with a new id).""" + sess = cherrypy.serving.session + sess.regenerate() + + # Grab cookie-relevant tool args + conf = dict([(k, v) for k, v in self._merged_args().items() + if k in ('path', 'path_header', 'name', 'timeout', + 'domain', 'secure')]) + _sessions.set_response_cookie(**conf) + + + + +class XMLRPCController(object): + """A Controller (page handler collection) for XML-RPC. + + To use it, have your controllers subclass this base class (it will + turn on the tool for you). + + You can also supply the following optional config entries:: + + tools.xmlrpc.encoding: 'utf-8' + tools.xmlrpc.allow_none: 0 + + XML-RPC is a rather discontinuous layer over HTTP; dispatching to the + appropriate handler must first be performed according to the URL, and + then a second dispatch step must take place according to the RPC method + specified in the request body. It also allows a superfluous "/RPC2" + prefix in the URL, supplies its own handler args in the body, and + requires a 200 OK "Fault" response instead of 404 when the desired + method is not found. + + Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone. + This Controller acts as the dispatch target for the first half (based + on the URL); it then reads the RPC method from the request body and + does its own second dispatch step based on that method. It also reads + body params, and returns a Fault on error. + + The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2 + in your URL's, you can safely skip turning on the XMLRPCDispatcher. + Otherwise, you need to use declare it in config:: + + request.dispatch: cherrypy.dispatch.XMLRPCDispatcher() + """ + + # Note we're hard-coding this into the 'tools' namespace. We could do + # a huge amount of work to make it relocatable, but the only reason why + # would be if someone actually disabled the default_toolbox. Meh. + _cp_config = {'tools.xmlrpc.on': True} + + def default(self, *vpath, **params): + rpcparams, rpcmethod = _xmlrpc.process_body() + + subhandler = self + for attr in str(rpcmethod).split('.'): + subhandler = getattr(subhandler, attr, None) + + if subhandler and getattr(subhandler, "exposed", False): + body = subhandler(*(vpath + rpcparams), **params) + + else: + # http://www.cherrypy.org/ticket/533 + # if a method is not found, an xmlrpclib.Fault should be returned + # raising an exception here will do that; see + # cherrypy.lib.xmlrpcutil.on_error + raise Exception('method "%s" is not supported' % attr) + + conf = cherrypy.serving.request.toolmaps['tools'].get("xmlrpc", {}) + _xmlrpc.respond(body, + conf.get('encoding', 'utf-8'), + conf.get('allow_none', 0)) + return cherrypy.serving.response.body + default.exposed = True + + +class SessionAuthTool(HandlerTool): + + def _setargs(self): + for name in dir(cptools.SessionAuth): + if not name.startswith("__"): + setattr(self, name, None) + + +class CachingTool(Tool): + """Caching Tool for CherryPy.""" + + def _wrapper(self, **kwargs): + request = cherrypy.serving.request + if _caching.get(**kwargs): + request.handler = None + else: + if request.cacheable: + # Note the devious technique here of adding hooks on the fly + request.hooks.attach('before_finalize', _caching.tee_output, + priority = 90) + _wrapper.priority = 20 + + def _setup(self): + """Hook caching into cherrypy.request.""" + conf = self._merged_args() + + p = conf.pop("priority", None) + cherrypy.serving.request.hooks.attach('before_handler', self._wrapper, + priority=p, **conf) + + + +class Toolbox(object): + """A collection of Tools. + + This object also functions as a config namespace handler for itself. + Custom toolboxes should be added to each Application's toolboxes dict. + """ + + def __init__(self, namespace): + self.namespace = namespace + + def __setattr__(self, name, value): + # If the Tool._name is None, supply it from the attribute name. + if isinstance(value, Tool): + if value._name is None: + value._name = name + value.namespace = self.namespace + object.__setattr__(self, name, value) + + def __enter__(self): + """Populate request.toolmaps from tools specified in config.""" + cherrypy.serving.request.toolmaps[self.namespace] = map = {} + def populate(k, v): + toolname, arg = k.split(".", 1) + bucket = map.setdefault(toolname, {}) + bucket[arg] = v + return populate + + def __exit__(self, exc_type, exc_val, exc_tb): + """Run tool._setup() for each tool in our toolmap.""" + map = cherrypy.serving.request.toolmaps.get(self.namespace) + if map: + for name, settings in map.items(): + if settings.get("on", False): + tool = getattr(self, name) + tool._setup() + + +class DeprecatedTool(Tool): + + _name = None + warnmsg = "This Tool is deprecated." + + def __init__(self, point, warnmsg=None): + self.point = point + if warnmsg is not None: + self.warnmsg = warnmsg + + def __call__(self, *args, **kwargs): + warnings.warn(self.warnmsg) + def tool_decorator(f): + return f + return tool_decorator + + def _setup(self): + warnings.warn(self.warnmsg) + + +default_toolbox = _d = Toolbox("tools") +_d.session_auth = SessionAuthTool(cptools.session_auth) +_d.allow = Tool('on_start_resource', cptools.allow) +_d.proxy = Tool('before_request_body', cptools.proxy, priority=30) +_d.response_headers = Tool('on_start_resource', cptools.response_headers) +_d.log_tracebacks = Tool('before_error_response', cptools.log_traceback) +_d.log_headers = Tool('before_error_response', cptools.log_request_headers) +_d.log_hooks = Tool('on_end_request', cptools.log_hooks, priority=100) +_d.err_redirect = ErrorTool(cptools.redirect) +_d.etags = Tool('before_finalize', cptools.validate_etags, priority=75) +_d.decode = Tool('before_request_body', encoding.decode) +# the order of encoding, gzip, caching is important +_d.encode = Tool('before_handler', encoding.ResponseEncoder, priority=70) +_d.gzip = Tool('before_finalize', encoding.gzip, priority=80) +_d.staticdir = HandlerTool(static.staticdir) +_d.staticfile = HandlerTool(static.staticfile) +_d.sessions = SessionTool() +_d.xmlrpc = ErrorTool(_xmlrpc.on_error) +_d.caching = CachingTool('before_handler', _caching.get, 'caching') +_d.expires = Tool('before_finalize', _caching.expires) +_d.tidy = DeprecatedTool('before_finalize', + "The tidy tool has been removed from the standard distribution of CherryPy. " + "The most recent version can be found at http://tools.cherrypy.org/browser.") +_d.nsgmls = DeprecatedTool('before_finalize', + "The nsgmls tool has been removed from the standard distribution of CherryPy. " + "The most recent version can be found at http://tools.cherrypy.org/browser.") +_d.ignore_headers = Tool('before_request_body', cptools.ignore_headers) +_d.referer = Tool('before_request_body', cptools.referer) +_d.basic_auth = Tool('on_start_resource', auth.basic_auth) +_d.digest_auth = Tool('on_start_resource', auth.digest_auth) +_d.trailing_slash = Tool('before_handler', cptools.trailing_slash, priority=60) +_d.flatten = Tool('before_finalize', cptools.flatten) +_d.accept = Tool('on_start_resource', cptools.accept) +_d.redirect = Tool('on_start_resource', cptools.redirect) +_d.autovary = Tool('on_start_resource', cptools.autovary, priority=0) +_d.json_in = Tool('before_request_body', jsontools.json_in, priority=30) +_d.json_out = Tool('before_handler', jsontools.json_out, priority=30) +_d.auth_basic = Tool('before_handler', auth_basic.basic_auth, priority=1) +_d.auth_digest = Tool('before_handler', auth_digest.digest_auth, priority=1) + +del _d, cptools, encoding, auth, static diff --git a/libs/cherrypy/_cptree.py b/libs/cherrypy/_cptree.py new file mode 100644 index 0000000..3aa4b9e --- /dev/null +++ b/libs/cherrypy/_cptree.py @@ -0,0 +1,290 @@ +"""CherryPy Application and Tree objects.""" + +import os +import sys + +import cherrypy +from cherrypy._cpcompat import ntou, py3k +from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools +from cherrypy.lib import httputil + + +class Application(object): + """A CherryPy Application. + + Servers and gateways should not instantiate Request objects directly. + Instead, they should ask an Application object for a request object. + + An instance of this class may also be used as a WSGI callable + (WSGI application object) for itself. + """ + + root = None + """The top-most container of page handlers for this app. Handlers should + be arranged in a hierarchy of attributes, matching the expected URI + hierarchy; the default dispatcher then searches this hierarchy for a + matching handler. When using a dispatcher other than the default, + this value may be None.""" + + config = {} + """A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict + of {key: value} pairs.""" + + namespaces = _cpconfig.NamespaceSet() + toolboxes = {'tools': cherrypy.tools} + + log = None + """A LogManager instance. See _cplogging.""" + + wsgiapp = None + """A CPWSGIApp instance. See _cpwsgi.""" + + request_class = _cprequest.Request + response_class = _cprequest.Response + + relative_urls = False + + def __init__(self, root, script_name="", config=None): + self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root) + self.root = root + self.script_name = script_name + self.wsgiapp = _cpwsgi.CPWSGIApp(self) + + self.namespaces = self.namespaces.copy() + self.namespaces["log"] = lambda k, v: setattr(self.log, k, v) + self.namespaces["wsgi"] = self.wsgiapp.namespace_handler + + self.config = self.__class__.config.copy() + if config: + self.merge(config) + + def __repr__(self): + return "%s.%s(%r, %r)" % (self.__module__, self.__class__.__name__, + self.root, self.script_name) + + script_name_doc = """The URI "mount point" for this app. A mount point is that portion of + the URI which is constant for all URIs that are serviced by this + application; it does not include scheme, host, or proxy ("virtual host") + portions of the URI. + + For example, if script_name is "/my/cool/app", then the URL + "http://www.example.com/my/cool/app/page1" might be handled by a + "page1" method on the root object. + + The value of script_name MUST NOT end in a slash. If the script_name + refers to the root of the URI, it MUST be an empty string (not "/"). + + If script_name is explicitly set to None, then the script_name will be + provided for each call from request.wsgi_environ['SCRIPT_NAME']. + """ + def _get_script_name(self): + if self._script_name is None: + # None signals that the script name should be pulled from WSGI environ. + return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip("/") + return self._script_name + def _set_script_name(self, value): + if value: + value = value.rstrip("/") + self._script_name = value + script_name = property(fget=_get_script_name, fset=_set_script_name, + doc=script_name_doc) + + def merge(self, config): + """Merge the given config into self.config.""" + _cpconfig.merge(self.config, config) + + # Handle namespaces specified in config. + self.namespaces(self.config.get("/", {})) + + def find_config(self, path, key, default=None): + """Return the most-specific value for key along path, or default.""" + trail = path or "/" + while trail: + nodeconf = self.config.get(trail, {}) + + if key in nodeconf: + return nodeconf[key] + + lastslash = trail.rfind("/") + if lastslash == -1: + break + elif lastslash == 0 and trail != "/": + trail = "/" + else: + trail = trail[:lastslash] + + return default + + def get_serving(self, local, remote, scheme, sproto): + """Create and return a Request and Response object.""" + req = self.request_class(local, remote, scheme, sproto) + req.app = self + + for name, toolbox in self.toolboxes.items(): + req.namespaces[name] = toolbox + + resp = self.response_class() + cherrypy.serving.load(req, resp) + cherrypy.engine.publish('acquire_thread') + cherrypy.engine.publish('before_request') + + return req, resp + + def release_serving(self): + """Release the current serving (request and response).""" + req = cherrypy.serving.request + + cherrypy.engine.publish('after_request') + + try: + req.close() + except: + cherrypy.log(traceback=True, severity=40) + + cherrypy.serving.clear() + + def __call__(self, environ, start_response): + return self.wsgiapp(environ, start_response) + + +class Tree(object): + """A registry of CherryPy applications, mounted at diverse points. + + An instance of this class may also be used as a WSGI callable + (WSGI application object), in which case it dispatches to all + mounted apps. + """ + + apps = {} + """ + A dict of the form {script name: application}, where "script name" + is a string declaring the URI mount point (no trailing slash), and + "application" is an instance of cherrypy.Application (or an arbitrary + WSGI callable if you happen to be using a WSGI server).""" + + def __init__(self): + self.apps = {} + + def mount(self, root, script_name="", config=None): + """Mount a new app from a root object, script_name, and config. + + root + An instance of a "controller class" (a collection of page + handler methods) which represents the root of the application. + This may also be an Application instance, or None if using + a dispatcher other than the default. + + script_name + A string containing the "mount point" of the application. + This should start with a slash, and be the path portion of the + URL at which to mount the given root. For example, if root.index() + will handle requests to "http://www.example.com:8080/dept/app1/", + then the script_name argument would be "/dept/app1". + + It MUST NOT end in a slash. If the script_name refers to the + root of the URI, it MUST be an empty string (not "/"). + + config + A file or dict containing application config. + """ + if script_name is None: + raise TypeError( + "The 'script_name' argument may not be None. Application " + "objects may, however, possess a script_name of None (in " + "order to inpect the WSGI environ for SCRIPT_NAME upon each " + "request). You cannot mount such Applications on this Tree; " + "you must pass them to a WSGI server interface directly.") + + # Next line both 1) strips trailing slash and 2) maps "/" -> "". + script_name = script_name.rstrip("/") + + if isinstance(root, Application): + app = root + if script_name != "" and script_name != app.script_name: + raise ValueError("Cannot specify a different script name and " + "pass an Application instance to cherrypy.mount") + script_name = app.script_name + else: + app = Application(root, script_name) + + # If mounted at "", add favicon.ico + if (script_name == "" and root is not None + and not hasattr(root, "favicon_ico")): + favicon = os.path.join(os.getcwd(), os.path.dirname(__file__), + "favicon.ico") + root.favicon_ico = tools.staticfile.handler(favicon) + + if config: + app.merge(config) + + self.apps[script_name] = app + + return app + + def graft(self, wsgi_callable, script_name=""): + """Mount a wsgi callable at the given script_name.""" + # Next line both 1) strips trailing slash and 2) maps "/" -> "". + script_name = script_name.rstrip("/") + self.apps[script_name] = wsgi_callable + + def script_name(self, path=None): + """The script_name of the app at the given path, or None. + + If path is None, cherrypy.request is used. + """ + if path is None: + try: + request = cherrypy.serving.request + path = httputil.urljoin(request.script_name, + request.path_info) + except AttributeError: + return None + + while True: + if path in self.apps: + return path + + if path == "": + return None + + # Move one node up the tree and try again. + path = path[:path.rfind("/")] + + def __call__(self, environ, start_response): + # If you're calling this, then you're probably setting SCRIPT_NAME + # to '' (some WSGI servers always set SCRIPT_NAME to ''). + # Try to look up the app using the full path. + env1x = environ + if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): + env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ) + path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''), + env1x.get('PATH_INFO', '')) + sn = self.script_name(path or "/") + if sn is None: + start_response('404 Not Found', []) + return [] + + app = self.apps[sn] + + # Correct the SCRIPT_NAME and PATH_INFO environ entries. + environ = environ.copy() + if not py3k: + if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): + # Python 2/WSGI u.0: all strings MUST be of type unicode + enc = environ[ntou('wsgi.url_encoding')] + environ[ntou('SCRIPT_NAME')] = sn.decode(enc) + environ[ntou('PATH_INFO')] = path[len(sn.rstrip("/")):].decode(enc) + else: + # Python 2/WSGI 1.x: all strings MUST be of type str + environ['SCRIPT_NAME'] = sn + environ['PATH_INFO'] = path[len(sn.rstrip("/")):] + else: + if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): + # Python 3/WSGI u.0: all strings MUST be full unicode + environ['SCRIPT_NAME'] = sn + environ['PATH_INFO'] = path[len(sn.rstrip("/")):] + else: + # Python 3/WSGI 1.x: all strings MUST be ISO-8859-1 str + environ['SCRIPT_NAME'] = sn.encode('utf-8').decode('ISO-8859-1') + environ['PATH_INFO'] = path[len(sn.rstrip("/")):].encode('utf-8').decode('ISO-8859-1') + return app(environ, start_response) diff --git a/libs/cherrypy/_cpwsgi.py b/libs/cherrypy/_cpwsgi.py new file mode 100644 index 0000000..91cd044 --- /dev/null +++ b/libs/cherrypy/_cpwsgi.py @@ -0,0 +1,408 @@ +"""WSGI interface (see PEP 333 and 3333). + +Note that WSGI environ keys and values are 'native strings'; that is, +whatever the type of "" is. For Python 2, that's a byte string; for Python 3, +it's a unicode string. But PEP 3333 says: "even if Python's str type is +actually Unicode "under the hood", the content of native strings must +still be translatable to bytes via the Latin-1 encoding!" +""" + +import sys as _sys + +import cherrypy as _cherrypy +from cherrypy._cpcompat import BytesIO, bytestr, ntob, ntou, py3k, unicodestr +from cherrypy import _cperror +from cherrypy.lib import httputil + + +def downgrade_wsgi_ux_to_1x(environ): + """Return a new environ dict for WSGI 1.x from the given WSGI u.x environ.""" + env1x = {} + + url_encoding = environ[ntou('wsgi.url_encoding')] + for k, v in list(environ.items()): + if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]: + v = v.encode(url_encoding) + elif isinstance(v, unicodestr): + v = v.encode('ISO-8859-1') + env1x[k.encode('ISO-8859-1')] = v + + return env1x + + +class VirtualHost(object): + """Select a different WSGI application based on the Host header. + + This can be useful when running multiple sites within one CP server. + It allows several domains to point to different applications. For example:: + + root = Root() + RootApp = cherrypy.Application(root) + Domain2App = cherrypy.Application(root) + SecureApp = cherrypy.Application(Secure()) + + vhost = cherrypy._cpwsgi.VirtualHost(RootApp, + domains={'www.domain2.example': Domain2App, + 'www.domain2.example:443': SecureApp, + }) + + cherrypy.tree.graft(vhost) + """ + default = None + """Required. The default WSGI application.""" + + use_x_forwarded_host = True + """If True (the default), any "X-Forwarded-Host" + request header will be used instead of the "Host" header. This + is commonly added by HTTP servers (such as Apache) when proxying.""" + + domains = {} + """A dict of {host header value: application} pairs. + The incoming "Host" request header is looked up in this dict, + and, if a match is found, the corresponding WSGI application + will be called instead of the default. Note that you often need + separate entries for "example.com" and "www.example.com". + In addition, "Host" headers may contain the port number. + """ + + def __init__(self, default, domains=None, use_x_forwarded_host=True): + self.default = default + self.domains = domains or {} + self.use_x_forwarded_host = use_x_forwarded_host + + def __call__(self, environ, start_response): + domain = environ.get('HTTP_HOST', '') + if self.use_x_forwarded_host: + domain = environ.get("HTTP_X_FORWARDED_HOST", domain) + + nextapp = self.domains.get(domain) + if nextapp is None: + nextapp = self.default + return nextapp(environ, start_response) + + +class InternalRedirector(object): + """WSGI middleware that handles raised cherrypy.InternalRedirect.""" + + def __init__(self, nextapp, recursive=False): + self.nextapp = nextapp + self.recursive = recursive + + def __call__(self, environ, start_response): + redirections = [] + while True: + environ = environ.copy() + try: + return self.nextapp(environ, start_response) + except _cherrypy.InternalRedirect: + ir = _sys.exc_info()[1] + sn = environ.get('SCRIPT_NAME', '') + path = environ.get('PATH_INFO', '') + qs = environ.get('QUERY_STRING', '') + + # Add the *previous* path_info + qs to redirections. + old_uri = sn + path + if qs: + old_uri += "?" + qs + redirections.append(old_uri) + + if not self.recursive: + # Check to see if the new URI has been redirected to already + new_uri = sn + ir.path + if ir.query_string: + new_uri += "?" + ir.query_string + if new_uri in redirections: + ir.request.close() + raise RuntimeError("InternalRedirector visited the " + "same URL twice: %r" % new_uri) + + # Munge the environment and try again. + environ['REQUEST_METHOD'] = "GET" + environ['PATH_INFO'] = ir.path + environ['QUERY_STRING'] = ir.query_string + environ['wsgi.input'] = BytesIO() + environ['CONTENT_LENGTH'] = "0" + environ['cherrypy.previous_request'] = ir.request + + +class ExceptionTrapper(object): + """WSGI middleware that traps exceptions.""" + + def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)): + self.nextapp = nextapp + self.throws = throws + + def __call__(self, environ, start_response): + return _TrappedResponse(self.nextapp, environ, start_response, self.throws) + + +class _TrappedResponse(object): + + response = iter([]) + + def __init__(self, nextapp, environ, start_response, throws): + self.nextapp = nextapp + self.environ = environ + self.start_response = start_response + self.throws = throws + self.started_response = False + self.response = self.trap(self.nextapp, self.environ, self.start_response) + self.iter_response = iter(self.response) + + def __iter__(self): + self.started_response = True + return self + + if py3k: + def __next__(self): + return self.trap(next, self.iter_response) + else: + def next(self): + return self.trap(self.iter_response.next) + + def close(self): + if hasattr(self.response, 'close'): + self.response.close() + + def trap(self, func, *args, **kwargs): + try: + return func(*args, **kwargs) + except self.throws: + raise + except StopIteration: + raise + except: + tb = _cperror.format_exc() + #print('trapped (started %s):' % self.started_response, tb) + _cherrypy.log(tb, severity=40) + if not _cherrypy.request.show_tracebacks: + tb = "" + s, h, b = _cperror.bare_error(tb) + if py3k: + # What fun. + s = s.decode('ISO-8859-1') + h = [(k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) + for k, v in h] + if self.started_response: + # Empty our iterable (so future calls raise StopIteration) + self.iter_response = iter([]) + else: + self.iter_response = iter(b) + + try: + self.start_response(s, h, _sys.exc_info()) + except: + # "The application must not trap any exceptions raised by + # start_response, if it called start_response with exc_info. + # Instead, it should allow such exceptions to propagate + # back to the server or gateway." + # But we still log and call close() to clean up ourselves. + _cherrypy.log(traceback=True, severity=40) + raise + + if self.started_response: + return ntob("").join(b) + else: + return b + + +# WSGI-to-CP Adapter # + + +class AppResponse(object): + """WSGI response iterable for CherryPy applications.""" + + def __init__(self, environ, start_response, cpapp): + self.cpapp = cpapp + try: + if not py3k: + if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): + environ = downgrade_wsgi_ux_to_1x(environ) + self.environ = environ + self.run() + + r = _cherrypy.serving.response + + outstatus = r.output_status + if not isinstance(outstatus, bytestr): + raise TypeError("response.output_status is not a byte string.") + + outheaders = [] + for k, v in r.header_list: + if not isinstance(k, bytestr): + raise TypeError("response.header_list key %r is not a byte string." % k) + if not isinstance(v, bytestr): + raise TypeError("response.header_list value %r is not a byte string." % v) + outheaders.append((k, v)) + + if py3k: + # According to PEP 3333, when using Python 3, the response status + # and headers must be bytes masquerading as unicode; that is, they + # must be of type "str" but are restricted to code points in the + # "latin-1" set. + outstatus = outstatus.decode('ISO-8859-1') + outheaders = [(k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) + for k, v in outheaders] + + self.iter_response = iter(r.body) + self.write = start_response(outstatus, outheaders) + except: + self.close() + raise + + def __iter__(self): + return self + + if py3k: + def __next__(self): + return next(self.iter_response) + else: + def next(self): + return self.iter_response.next() + + def close(self): + """Close and de-reference the current request and response. (Core)""" + self.cpapp.release_serving() + + def run(self): + """Create a Request object using environ.""" + env = self.environ.get + + local = httputil.Host('', int(env('SERVER_PORT', 80)), + env('SERVER_NAME', '')) + remote = httputil.Host(env('REMOTE_ADDR', ''), + int(env('REMOTE_PORT', -1) or -1), + env('REMOTE_HOST', '')) + scheme = env('wsgi.url_scheme') + sproto = env('ACTUAL_SERVER_PROTOCOL', "HTTP/1.1") + request, resp = self.cpapp.get_serving(local, remote, scheme, sproto) + + # LOGON_USER is served by IIS, and is the name of the + # user after having been mapped to a local account. + # Both IIS and Apache set REMOTE_USER, when possible. + request.login = env('LOGON_USER') or env('REMOTE_USER') or None + request.multithread = self.environ['wsgi.multithread'] + request.multiprocess = self.environ['wsgi.multiprocess'] + request.wsgi_environ = self.environ + request.prev = env('cherrypy.previous_request', None) + + meth = self.environ['REQUEST_METHOD'] + + path = httputil.urljoin(self.environ.get('SCRIPT_NAME', ''), + self.environ.get('PATH_INFO', '')) + qs = self.environ.get('QUERY_STRING', '') + + if py3k: + # This isn't perfect; if the given PATH_INFO is in the wrong encoding, + # it may fail to match the appropriate config section URI. But meh. + old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1') + new_enc = self.cpapp.find_config(self.environ.get('PATH_INFO', ''), + "request.uri_encoding", 'utf-8') + if new_enc.lower() != old_enc.lower(): + # Even though the path and qs are unicode, the WSGI server is + # required by PEP 3333 to coerce them to ISO-8859-1 masquerading + # as unicode. So we have to encode back to bytes and then decode + # again using the "correct" encoding. + try: + u_path = path.encode(old_enc).decode(new_enc) + u_qs = qs.encode(old_enc).decode(new_enc) + except (UnicodeEncodeError, UnicodeDecodeError): + # Just pass them through without transcoding and hope. + pass + else: + # Only set transcoded values if they both succeed. + path = u_path + qs = u_qs + + rproto = self.environ.get('SERVER_PROTOCOL') + headers = self.translate_headers(self.environ) + rfile = self.environ['wsgi.input'] + request.run(meth, path, qs, rproto, headers, rfile) + + headerNames = {'HTTP_CGI_AUTHORIZATION': 'Authorization', + 'CONTENT_LENGTH': 'Content-Length', + 'CONTENT_TYPE': 'Content-Type', + 'REMOTE_HOST': 'Remote-Host', + 'REMOTE_ADDR': 'Remote-Addr', + } + + def translate_headers(self, environ): + """Translate CGI-environ header names to HTTP header names.""" + for cgiName in environ: + # We assume all incoming header keys are uppercase already. + if cgiName in self.headerNames: + yield self.headerNames[cgiName], environ[cgiName] + elif cgiName[:5] == "HTTP_": + # Hackish attempt at recovering original header names. + translatedHeader = cgiName[5:].replace("_", "-") + yield translatedHeader, environ[cgiName] + + +class CPWSGIApp(object): + """A WSGI application object for a CherryPy Application.""" + + pipeline = [('ExceptionTrapper', ExceptionTrapper), + ('InternalRedirector', InternalRedirector), + ] + """A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a + constructor that takes an initial, positional 'nextapp' argument, + plus optional keyword arguments, and returns a WSGI application + (that takes environ and start_response arguments). The 'name' can + be any you choose, and will correspond to keys in self.config.""" + + head = None + """Rather than nest all apps in the pipeline on each call, it's only + done the first time, and the result is memoized into self.head. Set + this to None again if you change self.pipeline after calling self.""" + + config = {} + """A dict whose keys match names listed in the pipeline. Each + value is a further dict which will be passed to the corresponding + named WSGI callable (from the pipeline) as keyword arguments.""" + + response_class = AppResponse + """The class to instantiate and return as the next app in the WSGI chain.""" + + def __init__(self, cpapp, pipeline=None): + self.cpapp = cpapp + self.pipeline = self.pipeline[:] + if pipeline: + self.pipeline.extend(pipeline) + self.config = self.config.copy() + + def tail(self, environ, start_response): + """WSGI application callable for the actual CherryPy application. + + You probably shouldn't call this; call self.__call__ instead, + so that any WSGI middleware in self.pipeline can run first. + """ + return self.response_class(environ, start_response, self.cpapp) + + def __call__(self, environ, start_response): + head = self.head + if head is None: + # Create and nest the WSGI apps in our pipeline (in reverse order). + # Then memoize the result in self.head. + head = self.tail + for name, callable in self.pipeline[::-1]: + conf = self.config.get(name, {}) + head = callable(head, **conf) + self.head = head + return head(environ, start_response) + + def namespace_handler(self, k, v): + """Config handler for the 'wsgi' namespace.""" + if k == "pipeline": + # Note this allows multiple 'wsgi.pipeline' config entries + # (but each entry will be processed in a 'random' order). + # It should also allow developers to set default middleware + # in code (passed to self.__init__) that deployers can add to + # (but not remove) via config. + self.pipeline.extend(v) + elif k == "response_class": + self.response_class = v + else: + name, arg = k.split(".", 1) + bucket = self.config.setdefault(name, {}) + bucket[arg] = v + diff --git a/libs/cherrypy/_cpwsgi_server.py b/libs/cherrypy/_cpwsgi_server.py new file mode 100644 index 0000000..21af513 --- /dev/null +++ b/libs/cherrypy/_cpwsgi_server.py @@ -0,0 +1,63 @@ +"""WSGI server interface (see PEP 333). This adds some CP-specific bits to +the framework-agnostic wsgiserver package. +""" +import sys + +import cherrypy +from cherrypy import wsgiserver + + +class CPWSGIServer(wsgiserver.CherryPyWSGIServer): + """Wrapper for wsgiserver.CherryPyWSGIServer. + + wsgiserver has been designed to not reference CherryPy in any way, + so that it can be used in other frameworks and applications. Therefore, + we wrap it here, so we can set our own mount points from cherrypy.tree + and apply some attributes from config -> cherrypy.server -> wsgiserver. + """ + + def __init__(self, server_adapter=cherrypy.server): + self.server_adapter = server_adapter + self.max_request_header_size = self.server_adapter.max_request_header_size or 0 + self.max_request_body_size = self.server_adapter.max_request_body_size or 0 + + server_name = (self.server_adapter.socket_host or + self.server_adapter.socket_file or + None) + + self.wsgi_version = self.server_adapter.wsgi_version + s = wsgiserver.CherryPyWSGIServer + s.__init__(self, server_adapter.bind_addr, cherrypy.tree, + self.server_adapter.thread_pool, + server_name, + max = self.server_adapter.thread_pool_max, + request_queue_size = self.server_adapter.socket_queue_size, + timeout = self.server_adapter.socket_timeout, + shutdown_timeout = self.server_adapter.shutdown_timeout, + ) + self.protocol = self.server_adapter.protocol_version + self.nodelay = self.server_adapter.nodelay + + if sys.version_info >= (3, 0): + ssl_module = self.server_adapter.ssl_module or 'builtin' + else: + ssl_module = self.server_adapter.ssl_module or 'pyopenssl' + if self.server_adapter.ssl_context: + adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain) + self.ssl_adapter.context = self.server_adapter.ssl_context + elif self.server_adapter.ssl_certificate: + adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain) + + self.stats['Enabled'] = getattr(self.server_adapter, 'statistics', False) + + def error_log(self, msg="", level=20, traceback=False): + cherrypy.engine.log(msg, level, traceback) + diff --git a/libs/cherrypy/cherryd b/libs/cherrypy/cherryd new file mode 100644 index 0000000..adb2a02 --- /dev/null +++ b/libs/cherrypy/cherryd @@ -0,0 +1,109 @@ +#! /usr/bin/env python +"""The CherryPy daemon.""" + +import sys + +import cherrypy +from cherrypy.process import plugins, servers +from cherrypy import Application + +def start(configfiles=None, daemonize=False, environment=None, + fastcgi=False, scgi=False, pidfile=None, imports=None, + cgi=False): + """Subscribe all engine plugins and start the engine.""" + sys.path = [''] + sys.path + for i in imports or []: + exec("import %s" % i) + + for c in configfiles or []: + cherrypy.config.update(c) + # If there's only one app mounted, merge config into it. + if len(cherrypy.tree.apps) == 1: + for app in cherrypy.tree.apps.values(): + if isinstance(app, Application): + app.merge(c) + + engine = cherrypy.engine + + if environment is not None: + cherrypy.config.update({'environment': environment}) + + # Only daemonize if asked to. + if daemonize: + # Don't print anything to stdout/sterr. + cherrypy.config.update({'log.screen': False}) + plugins.Daemonizer(engine).subscribe() + + if pidfile: + plugins.PIDFile(engine, pidfile).subscribe() + + if hasattr(engine, "signal_handler"): + engine.signal_handler.subscribe() + if hasattr(engine, "console_control_handler"): + engine.console_control_handler.subscribe() + + if (fastcgi and (scgi or cgi)) or (scgi and cgi): + cherrypy.log.error("You may only specify one of the cgi, fastcgi, and " + "scgi options.", 'ENGINE') + sys.exit(1) + elif fastcgi or scgi or cgi: + # Turn off autoreload when using *cgi. + cherrypy.config.update({'engine.autoreload_on': False}) + # Turn off the default HTTP server (which is subscribed by default). + cherrypy.server.unsubscribe() + + addr = cherrypy.server.bind_addr + if fastcgi: + f = servers.FlupFCGIServer(application=cherrypy.tree, + bindAddress=addr) + elif scgi: + f = servers.FlupSCGIServer(application=cherrypy.tree, + bindAddress=addr) + else: + f = servers.FlupCGIServer(application=cherrypy.tree, + bindAddress=addr) + s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr) + s.subscribe() + + # Always start the engine; this will start all other services + try: + engine.start() + except: + # Assume the error has been logged already via bus.log. + sys.exit(1) + else: + engine.block() + + +if __name__ == '__main__': + from optparse import OptionParser + + p = OptionParser() + p.add_option('-c', '--config', action="append", dest='config', + help="specify config file(s)") + p.add_option('-d', action="store_true", dest='daemonize', + help="run the server as a daemon") + p.add_option('-e', '--environment', dest='environment', default=None, + help="apply the given config environment") + p.add_option('-f', action="store_true", dest='fastcgi', + help="start a fastcgi server instead of the default HTTP server") + p.add_option('-s', action="store_true", dest='scgi', + help="start a scgi server instead of the default HTTP server") + p.add_option('-x', action="store_true", dest='cgi', + help="start a cgi server instead of the default HTTP server") + p.add_option('-i', '--import', action="append", dest='imports', + help="specify modules to import") + p.add_option('-p', '--pidfile', dest='pidfile', default=None, + help="store the process id in the given file") + p.add_option('-P', '--Path', action="append", dest='Path', + help="add the given paths to sys.path") + options, args = p.parse_args() + + if options.Path: + for p in options.Path: + sys.path.insert(0, p) + + start(options.config, options.daemonize, + options.environment, options.fastcgi, options.scgi, + options.pidfile, options.imports, options.cgi) + diff --git a/libs/cherrypy/favicon.ico b/libs/cherrypy/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f0d7e61badad3f332cf1e663efb97c0b5be80f5e GIT binary patch literal 1406 zcmZQzU}Ruq5D);-91Iz(3=Con3=A3!3=9Gc3=9ek5OD@5@L*tI0AU73h`|5<{~2<3 zuVCo!zr&y+vzOueo97HSuYX}M)jh-@!n2cM$<-4K1zA@ZUcdasF!Ss|hOo6$7MITNg6?{`Z$5dczEc4?n*$eEs#4AtLA;gYSx7 zhRmfBrDo%&TYk{p%mYmRsi- zPCb3V@aNBehMI#L828PX#o+Dj&EVtX z!{F=d%i!nd$Kdbp&kzt0zz`T1$Pg41#1I@D%n%Y1!Vnr7$`BS7#tt`j$dHtj#E_hv%#f0j!jPJp%8-_p z#*m(#&XAFj!H}7m$&i(m#gLty&5)Cm!;qVs%aE6s$B>_&&rncMz))CN$WT;N#86yZ z%urHN!cbaT%1~BT#!y~f&QMWN!BAOQ$xu~Q#ZX;c%}`TQ!%$mW%TQNW$53Bi&(P4& zz|h#($k5c(#L(Q_%+S)(!qD2<%Fx!<#?ap0&d|}(!O+>+$KvI2cP_)cdGi?N&!5k*V8H^0g$oxlELyaPVe#U{3`>?QVOY9! zDZ{d5%NUj~U(T>%#R`U%D_1hCTD6K{_3G6OYu2n`Si5#D!@70r7}l>}&#+;`28N9r zH!^J6w25K!=FJRSwrpY8x^*kVwr$%Owr}6guw%y#hMhZiGVI#5i(&Wf-3)v7>|xlu zcQ3=feft>p@88dG;J^Wfg9i^X96EG};qc+Z3`dR}VK{p9D8sR1#~6+uKhALC#0iFz zCr>h*I(3TS^y$+KXU?2qID7Ui!?|DG(!@GCy7~a2s&+y^H2ZoOyKQes!^oil~=g$mZzIWrw{PDVzJLGD@Z-l1 zhMzxwGW`1Wi{bb0-wc2L{9*X}_b3x7W*K!5TM0yP zvNJO?vokR;Fz|9SFfeejF@u0S7lW!Ys0d+|XJ%$rlwe~O 0: + chunk = fileobj.read(min(chunk_size, remaining)) + chunklen = len(chunk) + if chunklen == 0: + return + remaining -= chunklen + yield chunk + +def set_vary_header(response, header_name): + "Add a Vary header to a response" + varies = response.headers.get("Vary", "") + varies = [x.strip() for x in varies.split(",") if x.strip()] + if header_name not in varies: + varies.append(header_name) + response.headers['Vary'] = ", ".join(varies) diff --git a/libs/cherrypy/lib/auth.py b/libs/cherrypy/lib/auth.py new file mode 100644 index 0000000..7d2f6dc --- /dev/null +++ b/libs/cherrypy/lib/auth.py @@ -0,0 +1,87 @@ +import cherrypy +from cherrypy.lib import httpauth + + +def check_auth(users, encrypt=None, realm=None): + """If an authorization header contains credentials, return True, else False.""" + request = cherrypy.serving.request + if 'authorization' in request.headers: + # make sure the provided credentials are correctly set + ah = httpauth.parseAuthorization(request.headers['authorization']) + if ah is None: + raise cherrypy.HTTPError(400, 'Bad Request') + + if not encrypt: + encrypt = httpauth.DIGEST_AUTH_ENCODERS[httpauth.MD5] + + if hasattr(users, '__call__'): + try: + # backward compatibility + users = users() # expect it to return a dictionary + + if not isinstance(users, dict): + raise ValueError("Authentication users must be a dictionary") + + # fetch the user password + password = users.get(ah["username"], None) + except TypeError: + # returns a password (encrypted or clear text) + password = users(ah["username"]) + else: + if not isinstance(users, dict): + raise ValueError("Authentication users must be a dictionary") + + # fetch the user password + password = users.get(ah["username"], None) + + # validate the authorization by re-computing it here + # and compare it with what the user-agent provided + if httpauth.checkResponse(ah, password, method=request.method, + encrypt=encrypt, realm=realm): + request.login = ah["username"] + return True + + request.login = False + return False + +def basic_auth(realm, users, encrypt=None, debug=False): + """If auth fails, raise 401 with a basic authentication header. + + realm + A string containing the authentication realm. + + users + A dict of the form: {username: password} or a callable returning a dict. + + encrypt + callable used to encrypt the password returned from the user-agent. + if None it defaults to a md5 encryption. + + """ + if check_auth(users, encrypt): + if debug: + cherrypy.log('Auth successful', 'TOOLS.BASIC_AUTH') + return + + # inform the user-agent this path is protected + cherrypy.serving.response.headers['www-authenticate'] = httpauth.basicAuth(realm) + + raise cherrypy.HTTPError(401, "You are not authorized to access that resource") + +def digest_auth(realm, users, debug=False): + """If auth fails, raise 401 with a digest authentication header. + + realm + A string containing the authentication realm. + users + A dict of the form: {username: password} or a callable returning a dict. + """ + if check_auth(users, realm=realm): + if debug: + cherrypy.log('Auth successful', 'TOOLS.DIGEST_AUTH') + return + + # inform the user-agent this path is protected + cherrypy.serving.response.headers['www-authenticate'] = httpauth.digestAuth(realm) + + raise cherrypy.HTTPError(401, "You are not authorized to access that resource") diff --git a/libs/cherrypy/lib/auth_basic.py b/libs/cherrypy/lib/auth_basic.py new file mode 100644 index 0000000..2c05e01 --- /dev/null +++ b/libs/cherrypy/lib/auth_basic.py @@ -0,0 +1,87 @@ +# This file is part of CherryPy +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 + +__doc__ = """This module provides a CherryPy 3.x tool which implements +the server-side of HTTP Basic Access Authentication, as described in :rfc:`2617`. + +Example usage, using the built-in checkpassword_dict function which uses a dict +as the credentials store:: + + userpassdict = {'bird' : 'bebop', 'ornette' : 'wayout'} + checkpassword = cherrypy.lib.auth_basic.checkpassword_dict(userpassdict) + basic_auth = {'tools.auth_basic.on': True, + 'tools.auth_basic.realm': 'earth', + 'tools.auth_basic.checkpassword': checkpassword, + } + app_config = { '/' : basic_auth } + +""" + +__author__ = 'visteya' +__date__ = 'April 2009' + +import binascii +from cherrypy._cpcompat import base64_decode +import cherrypy + + +def checkpassword_dict(user_password_dict): + """Returns a checkpassword function which checks credentials + against a dictionary of the form: {username : password}. + + If you want a simple dictionary-based authentication scheme, use + checkpassword_dict(my_credentials_dict) as the value for the + checkpassword argument to basic_auth(). + """ + def checkpassword(realm, user, password): + p = user_password_dict.get(user) + return p and p == password or False + + return checkpassword + + +def basic_auth(realm, checkpassword, debug=False): + """A CherryPy tool which hooks at before_handler to perform + HTTP Basic Access Authentication, as specified in :rfc:`2617`. + + If the request has an 'authorization' header with a 'Basic' scheme, this + tool attempts to authenticate the credentials supplied in that header. If + the request has no 'authorization' header, or if it does but the scheme is + not 'Basic', or if authentication fails, the tool sends a 401 response with + a 'WWW-Authenticate' Basic header. + + realm + A string containing the authentication realm. + + checkpassword + A callable which checks the authentication credentials. + Its signature is checkpassword(realm, username, password). where + username and password are the values obtained from the request's + 'authorization' header. If authentication succeeds, checkpassword + returns True, else it returns False. + + """ + + if '"' in realm: + raise ValueError('Realm cannot contain the " (quote) character.') + request = cherrypy.serving.request + + auth_header = request.headers.get('authorization') + if auth_header is not None: + try: + scheme, params = auth_header.split(' ', 1) + if scheme.lower() == 'basic': + username, password = base64_decode(params).split(':', 1) + if checkpassword(realm, username, password): + if debug: + cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC') + request.login = username + return # successful authentication + except (ValueError, binascii.Error): # split() error, base64.decodestring() error + raise cherrypy.HTTPError(400, 'Bad Request') + + # Respond with 401 status and a WWW-Authenticate header + cherrypy.serving.response.headers['www-authenticate'] = 'Basic realm="%s"' % realm + raise cherrypy.HTTPError(401, "You are not authorized to access that resource") + diff --git a/libs/cherrypy/lib/auth_digest.py b/libs/cherrypy/lib/auth_digest.py new file mode 100644 index 0000000..67578e0 --- /dev/null +++ b/libs/cherrypy/lib/auth_digest.py @@ -0,0 +1,365 @@ +# This file is part of CherryPy +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 + +__doc__ = """An implementation of the server-side of HTTP Digest Access +Authentication, which is described in :rfc:`2617`. + +Example usage, using the built-in get_ha1_dict_plain function which uses a dict +of plaintext passwords as the credentials store:: + + userpassdict = {'alice' : '4x5istwelve'} + get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(userpassdict) + digest_auth = {'tools.auth_digest.on': True, + 'tools.auth_digest.realm': 'wonderland', + 'tools.auth_digest.get_ha1': get_ha1, + 'tools.auth_digest.key': 'a565c27146791cfb', + } + app_config = { '/' : digest_auth } +""" + +__author__ = 'visteya' +__date__ = 'April 2009' + + +import time +from cherrypy._cpcompat import parse_http_list, parse_keqv_list + +import cherrypy +from cherrypy._cpcompat import md5, ntob +md5_hex = lambda s: md5(ntob(s)).hexdigest() + +qop_auth = 'auth' +qop_auth_int = 'auth-int' +valid_qops = (qop_auth, qop_auth_int) + +valid_algorithms = ('MD5', 'MD5-sess') + + +def TRACE(msg): + cherrypy.log(msg, context='TOOLS.AUTH_DIGEST') + +# Three helper functions for users of the tool, providing three variants +# of get_ha1() functions for three different kinds of credential stores. +def get_ha1_dict_plain(user_password_dict): + """Returns a get_ha1 function which obtains a plaintext password from a + dictionary of the form: {username : password}. + + If you want a simple dictionary-based authentication scheme, with plaintext + passwords, use get_ha1_dict_plain(my_userpass_dict) as the value for the + get_ha1 argument to digest_auth(). + """ + def get_ha1(realm, username): + password = user_password_dict.get(username) + if password: + return md5_hex('%s:%s:%s' % (username, realm, password)) + return None + + return get_ha1 + +def get_ha1_dict(user_ha1_dict): + """Returns a get_ha1 function which obtains a HA1 password hash from a + dictionary of the form: {username : HA1}. + + If you want a dictionary-based authentication scheme, but with + pre-computed HA1 hashes instead of plain-text passwords, use + get_ha1_dict(my_userha1_dict) as the value for the get_ha1 + argument to digest_auth(). + """ + def get_ha1(realm, username): + return user_ha1_dict.get(user) + + return get_ha1 + +def get_ha1_file_htdigest(filename): + """Returns a get_ha1 function which obtains a HA1 password hash from a + flat file with lines of the same format as that produced by the Apache + htdigest utility. For example, for realm 'wonderland', username 'alice', + and password '4x5istwelve', the htdigest line would be:: + + alice:wonderland:3238cdfe91a8b2ed8e39646921a02d4c + + If you want to use an Apache htdigest file as the credentials store, + then use get_ha1_file_htdigest(my_htdigest_file) as the value for the + get_ha1 argument to digest_auth(). It is recommended that the filename + argument be an absolute path, to avoid problems. + """ + def get_ha1(realm, username): + result = None + f = open(filename, 'r') + for line in f: + u, r, ha1 = line.rstrip().split(':') + if u == username and r == realm: + result = ha1 + break + f.close() + return result + + return get_ha1 + + +def synthesize_nonce(s, key, timestamp=None): + """Synthesize a nonce value which resists spoofing and can be checked for staleness. + Returns a string suitable as the value for 'nonce' in the www-authenticate header. + + s + A string related to the resource, such as the hostname of the server. + + key + A secret string known only to the server. + + timestamp + An integer seconds-since-the-epoch timestamp + + """ + if timestamp is None: + timestamp = int(time.time()) + h = md5_hex('%s:%s:%s' % (timestamp, s, key)) + nonce = '%s:%s' % (timestamp, h) + return nonce + + +def H(s): + """The hash function H""" + return md5_hex(s) + + +class HttpDigestAuthorization (object): + """Class to parse a Digest Authorization header and perform re-calculation + of the digest. + """ + + def errmsg(self, s): + return 'Digest Authorization header: %s' % s + + def __init__(self, auth_header, http_method, debug=False): + self.http_method = http_method + self.debug = debug + scheme, params = auth_header.split(" ", 1) + self.scheme = scheme.lower() + if self.scheme != 'digest': + raise ValueError('Authorization scheme is not "Digest"') + + self.auth_header = auth_header + + # make a dict of the params + items = parse_http_list(params) + paramsd = parse_keqv_list(items) + + self.realm = paramsd.get('realm') + self.username = paramsd.get('username') + self.nonce = paramsd.get('nonce') + self.uri = paramsd.get('uri') + self.method = paramsd.get('method') + self.response = paramsd.get('response') # the response digest + self.algorithm = paramsd.get('algorithm', 'MD5') + self.cnonce = paramsd.get('cnonce') + self.opaque = paramsd.get('opaque') + self.qop = paramsd.get('qop') # qop + self.nc = paramsd.get('nc') # nonce count + + # perform some correctness checks + if self.algorithm not in valid_algorithms: + raise ValueError(self.errmsg("Unsupported value for algorithm: '%s'" % self.algorithm)) + + has_reqd = self.username and \ + self.realm and \ + self.nonce and \ + self.uri and \ + self.response + if not has_reqd: + raise ValueError(self.errmsg("Not all required parameters are present.")) + + if self.qop: + if self.qop not in valid_qops: + raise ValueError(self.errmsg("Unsupported value for qop: '%s'" % self.qop)) + if not (self.cnonce and self.nc): + raise ValueError(self.errmsg("If qop is sent then cnonce and nc MUST be present")) + else: + if self.cnonce or self.nc: + raise ValueError(self.errmsg("If qop is not sent, neither cnonce nor nc can be present")) + + + def __str__(self): + return 'authorization : %s' % self.auth_header + + def validate_nonce(self, s, key): + """Validate the nonce. + Returns True if nonce was generated by synthesize_nonce() and the timestamp + is not spoofed, else returns False. + + s + A string related to the resource, such as the hostname of the server. + + key + A secret string known only to the server. + + Both s and key must be the same values which were used to synthesize the nonce + we are trying to validate. + """ + try: + timestamp, hashpart = self.nonce.split(':', 1) + s_timestamp, s_hashpart = synthesize_nonce(s, key, timestamp).split(':', 1) + is_valid = s_hashpart == hashpart + if self.debug: + TRACE('validate_nonce: %s' % is_valid) + return is_valid + except ValueError: # split() error + pass + return False + + + def is_nonce_stale(self, max_age_seconds=600): + """Returns True if a validated nonce is stale. The nonce contains a + timestamp in plaintext and also a secure hash of the timestamp. You should + first validate the nonce to ensure the plaintext timestamp is not spoofed. + """ + try: + timestamp, hashpart = self.nonce.split(':', 1) + if int(timestamp) + max_age_seconds > int(time.time()): + return False + except ValueError: # int() error + pass + if self.debug: + TRACE("nonce is stale") + return True + + + def HA2(self, entity_body=''): + """Returns the H(A2) string. See :rfc:`2617` section 3.2.2.3.""" + # RFC 2617 3.2.2.3 + # If the "qop" directive's value is "auth" or is unspecified, then A2 is: + # A2 = method ":" digest-uri-value + # + # If the "qop" value is "auth-int", then A2 is: + # A2 = method ":" digest-uri-value ":" H(entity-body) + if self.qop is None or self.qop == "auth": + a2 = '%s:%s' % (self.http_method, self.uri) + elif self.qop == "auth-int": + a2 = "%s:%s:%s" % (self.http_method, self.uri, H(entity_body)) + else: + # in theory, this should never happen, since I validate qop in __init__() + raise ValueError(self.errmsg("Unrecognized value for qop!")) + return H(a2) + + + def request_digest(self, ha1, entity_body=''): + """Calculates the Request-Digest. See :rfc:`2617` section 3.2.2.1. + + ha1 + The HA1 string obtained from the credentials store. + + entity_body + If 'qop' is set to 'auth-int', then A2 includes a hash + of the "entity body". The entity body is the part of the + message which follows the HTTP headers. See :rfc:`2617` section + 4.3. This refers to the entity the user agent sent in the request which + has the Authorization header. Typically GET requests don't have an entity, + and POST requests do. + + """ + ha2 = self.HA2(entity_body) + # Request-Digest -- RFC 2617 3.2.2.1 + if self.qop: + req = "%s:%s:%s:%s:%s" % (self.nonce, self.nc, self.cnonce, self.qop, ha2) + else: + req = "%s:%s" % (self.nonce, ha2) + + # RFC 2617 3.2.2.2 + # + # If the "algorithm" directive's value is "MD5" or is unspecified, then A1 is: + # A1 = unq(username-value) ":" unq(realm-value) ":" passwd + # + # If the "algorithm" directive's value is "MD5-sess", then A1 is + # calculated only once - on the first request by the client following + # receipt of a WWW-Authenticate challenge from the server. + # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd ) + # ":" unq(nonce-value) ":" unq(cnonce-value) + if self.algorithm == 'MD5-sess': + ha1 = H('%s:%s:%s' % (ha1, self.nonce, self.cnonce)) + + digest = H('%s:%s' % (ha1, req)) + return digest + + + +def www_authenticate(realm, key, algorithm='MD5', nonce=None, qop=qop_auth, stale=False): + """Constructs a WWW-Authenticate header for Digest authentication.""" + if qop not in valid_qops: + raise ValueError("Unsupported value for qop: '%s'" % qop) + if algorithm not in valid_algorithms: + raise ValueError("Unsupported value for algorithm: '%s'" % algorithm) + + if nonce is None: + nonce = synthesize_nonce(realm, key) + s = 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % ( + realm, nonce, algorithm, qop) + if stale: + s += ', stale="true"' + return s + + +def digest_auth(realm, get_ha1, key, debug=False): + """A CherryPy tool which hooks at before_handler to perform + HTTP Digest Access Authentication, as specified in :rfc:`2617`. + + If the request has an 'authorization' header with a 'Digest' scheme, this + tool authenticates the credentials supplied in that header. If + the request has no 'authorization' header, or if it does but the scheme is + not "Digest", or if authentication fails, the tool sends a 401 response with + a 'WWW-Authenticate' Digest header. + + realm + A string containing the authentication realm. + + get_ha1 + A callable which looks up a username in a credentials store + and returns the HA1 string, which is defined in the RFC to be + MD5(username : realm : password). The function's signature is: + ``get_ha1(realm, username)`` + where username is obtained from the request's 'authorization' header. + If username is not found in the credentials store, get_ha1() returns + None. + + key + A secret string known only to the server, used in the synthesis of nonces. + + """ + request = cherrypy.serving.request + + auth_header = request.headers.get('authorization') + nonce_is_stale = False + if auth_header is not None: + try: + auth = HttpDigestAuthorization(auth_header, request.method, debug=debug) + except ValueError: + raise cherrypy.HTTPError(400, "The Authorization header could not be parsed.") + + if debug: + TRACE(str(auth)) + + if auth.validate_nonce(realm, key): + ha1 = get_ha1(realm, auth.username) + if ha1 is not None: + # note that for request.body to be available we need to hook in at + # before_handler, not on_start_resource like 3.1.x digest_auth does. + digest = auth.request_digest(ha1, entity_body=request.body) + if digest == auth.response: # authenticated + if debug: + TRACE("digest matches auth.response") + # Now check if nonce is stale. + # The choice of ten minutes' lifetime for nonce is somewhat arbitrary + nonce_is_stale = auth.is_nonce_stale(max_age_seconds=600) + if not nonce_is_stale: + request.login = auth.username + if debug: + TRACE("authentication of %s successful" % auth.username) + return + + # Respond with 401 status and a WWW-Authenticate header + header = www_authenticate(realm, key, stale=nonce_is_stale) + if debug: + TRACE(header) + cherrypy.serving.response.headers['WWW-Authenticate'] = header + raise cherrypy.HTTPError(401, "You are not authorized to access that resource") + diff --git a/libs/cherrypy/lib/caching.py b/libs/cherrypy/lib/caching.py new file mode 100644 index 0000000..435b9dc --- /dev/null +++ b/libs/cherrypy/lib/caching.py @@ -0,0 +1,465 @@ +""" +CherryPy implements a simple caching system as a pluggable Tool. This tool tries +to be an (in-process) HTTP/1.1-compliant cache. It's not quite there yet, but +it's probably good enough for most sites. + +In general, GET responses are cached (along with selecting headers) and, if +another request arrives for the same resource, the caching Tool will return 304 +Not Modified if possible, or serve the cached response otherwise. It also sets +request.cached to True if serving a cached representation, and sets +request.cacheable to False (so it doesn't get cached again). + +If POST, PUT, or DELETE requests are made for a cached resource, they invalidate +(delete) any cached response. + +Usage +===== + +Configuration file example:: + + [/] + tools.caching.on = True + tools.caching.delay = 3600 + +You may use a class other than the default +:class:`MemoryCache` by supplying the config +entry ``cache_class``; supply the full dotted name of the replacement class +as the config value. It must implement the basic methods ``get``, ``put``, +``delete``, and ``clear``. + +You may set any attribute, including overriding methods, on the cache +instance by providing them in config. The above sets the +:attr:`delay` attribute, for example. +""" + +import datetime +import sys +import threading +import time + +import cherrypy +from cherrypy.lib import cptools, httputil +from cherrypy._cpcompat import copyitems, ntob, set_daemon, sorted + + +class Cache(object): + """Base class for Cache implementations.""" + + def get(self): + """Return the current variant if in the cache, else None.""" + raise NotImplemented + + def put(self, obj, size): + """Store the current variant in the cache.""" + raise NotImplemented + + def delete(self): + """Remove ALL cached variants of the current resource.""" + raise NotImplemented + + def clear(self): + """Reset the cache to its initial, empty state.""" + raise NotImplemented + + + +# ------------------------------- Memory Cache ------------------------------- # + + +class AntiStampedeCache(dict): + """A storage system for cached items which reduces stampede collisions.""" + + def wait(self, key, timeout=5, debug=False): + """Return the cached value for the given key, or None. + + If timeout is not None, and the value is already + being calculated by another thread, wait until the given timeout has + elapsed. If the value is available before the timeout expires, it is + returned. If not, None is returned, and a sentinel placed in the cache + to signal other threads to wait. + + If timeout is None, no waiting is performed nor sentinels used. + """ + value = self.get(key) + if isinstance(value, threading._Event): + if timeout is None: + # Ignore the other thread and recalc it ourselves. + if debug: + cherrypy.log('No timeout', 'TOOLS.CACHING') + return None + + # Wait until it's done or times out. + if debug: + cherrypy.log('Waiting up to %s seconds' % timeout, 'TOOLS.CACHING') + value.wait(timeout) + if value.result is not None: + # The other thread finished its calculation. Use it. + if debug: + cherrypy.log('Result!', 'TOOLS.CACHING') + return value.result + # Timed out. Stick an Event in the slot so other threads wait + # on this one to finish calculating the value. + if debug: + cherrypy.log('Timed out', 'TOOLS.CACHING') + e = threading.Event() + e.result = None + dict.__setitem__(self, key, e) + + return None + elif value is None: + # Stick an Event in the slot so other threads wait + # on this one to finish calculating the value. + if debug: + cherrypy.log('Timed out', 'TOOLS.CACHING') + e = threading.Event() + e.result = None + dict.__setitem__(self, key, e) + return value + + def __setitem__(self, key, value): + """Set the cached value for the given key.""" + existing = self.get(key) + dict.__setitem__(self, key, value) + if isinstance(existing, threading._Event): + # Set Event.result so other threads waiting on it have + # immediate access without needing to poll the cache again. + existing.result = value + existing.set() + + +class MemoryCache(Cache): + """An in-memory cache for varying response content. + + Each key in self.store is a URI, and each value is an AntiStampedeCache. + The response for any given URI may vary based on the values of + "selecting request headers"; that is, those named in the Vary + response header. We assume the list of header names to be constant + for each URI throughout the lifetime of the application, and store + that list in ``self.store[uri].selecting_headers``. + + The items contained in ``self.store[uri]`` have keys which are tuples of + request header values (in the same order as the names in its + selecting_headers), and values which are the actual responses. + """ + + maxobjects = 1000 + """The maximum number of cached objects; defaults to 1000.""" + + maxobj_size = 100000 + """The maximum size of each cached object in bytes; defaults to 100 KB.""" + + maxsize = 10000000 + """The maximum size of the entire cache in bytes; defaults to 10 MB.""" + + delay = 600 + """Seconds until the cached content expires; defaults to 600 (10 minutes).""" + + antistampede_timeout = 5 + """Seconds to wait for other threads to release a cache lock.""" + + expire_freq = 0.1 + """Seconds to sleep between cache expiration sweeps.""" + + debug = False + + def __init__(self): + self.clear() + + # Run self.expire_cache in a separate daemon thread. + t = threading.Thread(target=self.expire_cache, name='expire_cache') + self.expiration_thread = t + set_daemon(t, True) + t.start() + + def clear(self): + """Reset the cache to its initial, empty state.""" + self.store = {} + self.expirations = {} + self.tot_puts = 0 + self.tot_gets = 0 + self.tot_hist = 0 + self.tot_expires = 0 + self.tot_non_modified = 0 + self.cursize = 0 + + def expire_cache(self): + """Continuously examine cached objects, expiring stale ones. + + This function is designed to be run in its own daemon thread, + referenced at ``self.expiration_thread``. + """ + # It's possible that "time" will be set to None + # arbitrarily, so we check "while time" to avoid exceptions. + # See tickets #99 and #180 for more information. + while time: + now = time.time() + # Must make a copy of expirations so it doesn't change size + # during iteration + for expiration_time, objects in copyitems(self.expirations): + if expiration_time <= now: + for obj_size, uri, sel_header_values in objects: + try: + del self.store[uri][tuple(sel_header_values)] + self.tot_expires += 1 + self.cursize -= obj_size + except KeyError: + # the key may have been deleted elsewhere + pass + del self.expirations[expiration_time] + time.sleep(self.expire_freq) + + def get(self): + """Return the current variant if in the cache, else None.""" + request = cherrypy.serving.request + self.tot_gets += 1 + + uri = cherrypy.url(qs=request.query_string) + uricache = self.store.get(uri) + if uricache is None: + return None + + header_values = [request.headers.get(h, '') + for h in uricache.selecting_headers] + variant = uricache.wait(key=tuple(sorted(header_values)), + timeout=self.antistampede_timeout, + debug=self.debug) + if variant is not None: + self.tot_hist += 1 + return variant + + def put(self, variant, size): + """Store the current variant in the cache.""" + request = cherrypy.serving.request + response = cherrypy.serving.response + + uri = cherrypy.url(qs=request.query_string) + uricache = self.store.get(uri) + if uricache is None: + uricache = AntiStampedeCache() + uricache.selecting_headers = [ + e.value for e in response.headers.elements('Vary')] + self.store[uri] = uricache + + if len(self.store) < self.maxobjects: + total_size = self.cursize + size + + # checks if there's space for the object + if (size < self.maxobj_size and total_size < self.maxsize): + # add to the expirations list + expiration_time = response.time + self.delay + bucket = self.expirations.setdefault(expiration_time, []) + bucket.append((size, uri, uricache.selecting_headers)) + + # add to the cache + header_values = [request.headers.get(h, '') + for h in uricache.selecting_headers] + uricache[tuple(sorted(header_values))] = variant + self.tot_puts += 1 + self.cursize = total_size + + def delete(self): + """Remove ALL cached variants of the current resource.""" + uri = cherrypy.url(qs=cherrypy.serving.request.query_string) + self.store.pop(uri, None) + + +def get(invalid_methods=("POST", "PUT", "DELETE"), debug=False, **kwargs): + """Try to obtain cached output. If fresh enough, raise HTTPError(304). + + If POST, PUT, or DELETE: + * invalidates (deletes) any cached response for this resource + * sets request.cached = False + * sets request.cacheable = False + + else if a cached copy exists: + * sets request.cached = True + * sets request.cacheable = False + * sets response.headers to the cached values + * checks the cached Last-Modified response header against the + current If-(Un)Modified-Since request headers; raises 304 + if necessary. + * sets response.status and response.body to the cached values + * returns True + + otherwise: + * sets request.cached = False + * sets request.cacheable = True + * returns False + """ + request = cherrypy.serving.request + response = cherrypy.serving.response + + if not hasattr(cherrypy, "_cache"): + # Make a process-wide Cache object. + cherrypy._cache = kwargs.pop("cache_class", MemoryCache)() + + # Take all remaining kwargs and set them on the Cache object. + for k, v in kwargs.items(): + setattr(cherrypy._cache, k, v) + cherrypy._cache.debug = debug + + # POST, PUT, DELETE should invalidate (delete) the cached copy. + # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10. + if request.method in invalid_methods: + if debug: + cherrypy.log('request.method %r in invalid_methods %r' % + (request.method, invalid_methods), 'TOOLS.CACHING') + cherrypy._cache.delete() + request.cached = False + request.cacheable = False + return False + + if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]: + request.cached = False + request.cacheable = True + return False + + cache_data = cherrypy._cache.get() + request.cached = bool(cache_data) + request.cacheable = not request.cached + if request.cached: + # Serve the cached copy. + max_age = cherrypy._cache.delay + for v in [e.value for e in request.headers.elements('Cache-Control')]: + atoms = v.split('=', 1) + directive = atoms.pop(0) + if directive == 'max-age': + if len(atoms) != 1 or not atoms[0].isdigit(): + raise cherrypy.HTTPError(400, "Invalid Cache-Control header") + max_age = int(atoms[0]) + break + elif directive == 'no-cache': + if debug: + cherrypy.log('Ignoring cache due to Cache-Control: no-cache', + 'TOOLS.CACHING') + request.cached = False + request.cacheable = True + return False + + if debug: + cherrypy.log('Reading response from cache', 'TOOLS.CACHING') + s, h, b, create_time = cache_data + age = int(response.time - create_time) + if (age > max_age): + if debug: + cherrypy.log('Ignoring cache due to age > %d' % max_age, + 'TOOLS.CACHING') + request.cached = False + request.cacheable = True + return False + + # Copy the response headers. See http://www.cherrypy.org/ticket/721. + response.headers = rh = httputil.HeaderMap() + for k in h: + dict.__setitem__(rh, k, dict.__getitem__(h, k)) + + # Add the required Age header + response.headers["Age"] = str(age) + + try: + # Note that validate_since depends on a Last-Modified header; + # this was put into the cached copy, and should have been + # resurrected just above (response.headers = cache_data[1]). + cptools.validate_since() + except cherrypy.HTTPRedirect: + x = sys.exc_info()[1] + if x.status == 304: + cherrypy._cache.tot_non_modified += 1 + raise + + # serve it & get out from the request + response.status = s + response.body = b + else: + if debug: + cherrypy.log('request is not cached', 'TOOLS.CACHING') + return request.cached + + +def tee_output(): + """Tee response output to cache storage. Internal.""" + # Used by CachingTool by attaching to request.hooks + + request = cherrypy.serving.request + if 'no-store' in request.headers.values('Cache-Control'): + return + + def tee(body): + """Tee response.body into a list.""" + if ('no-cache' in response.headers.values('Pragma') or + 'no-store' in response.headers.values('Cache-Control')): + for chunk in body: + yield chunk + return + + output = [] + for chunk in body: + output.append(chunk) + yield chunk + + # save the cache data + body = ntob('').join(output) + cherrypy._cache.put((response.status, response.headers or {}, + body, response.time), len(body)) + + response = cherrypy.serving.response + response.body = tee(response.body) + + +def expires(secs=0, force=False, debug=False): + """Tool for influencing cache mechanisms using the 'Expires' header. + + secs + Must be either an int or a datetime.timedelta, and indicates the + number of seconds between response.time and when the response should + expire. The 'Expires' header will be set to response.time + secs. + If secs is zero, the 'Expires' header is set one year in the past, and + the following "cache prevention" headers are also set: + + * Pragma: no-cache + * Cache-Control': no-cache, must-revalidate + + force + If False, the following headers are checked: + + * Etag + * Last-Modified + * Age + * Expires + + If any are already present, none of the above response headers are set. + + """ + + response = cherrypy.serving.response + headers = response.headers + + cacheable = False + if not force: + # some header names that indicate that the response can be cached + for indicator in ('Etag', 'Last-Modified', 'Age', 'Expires'): + if indicator in headers: + cacheable = True + break + + if not cacheable and not force: + if debug: + cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES') + else: + if debug: + cherrypy.log('request is cacheable', 'TOOLS.EXPIRES') + if isinstance(secs, datetime.timedelta): + secs = (86400 * secs.days) + secs.seconds + + if secs == 0: + if force or ("Pragma" not in headers): + headers["Pragma"] = "no-cache" + if cherrypy.serving.request.protocol >= (1, 1): + if force or "Cache-Control" not in headers: + headers["Cache-Control"] = "no-cache, must-revalidate" + # Set an explicit Expires date in the past. + expiry = httputil.HTTPDate(1169942400.0) + else: + expiry = httputil.HTTPDate(response.time + secs) + if force or "Expires" not in headers: + headers["Expires"] = expiry diff --git a/libs/cherrypy/lib/covercp.py b/libs/cherrypy/lib/covercp.py new file mode 100644 index 0000000..9b701b5 --- /dev/null +++ b/libs/cherrypy/lib/covercp.py @@ -0,0 +1,365 @@ +"""Code-coverage tools for CherryPy. + +To use this module, or the coverage tools in the test suite, +you need to download 'coverage.py', either Gareth Rees' `original +implementation `_ +or Ned Batchelder's `enhanced version: +`_ + +To turn on coverage tracing, use the following code:: + + cherrypy.engine.subscribe('start', covercp.start) + +DO NOT subscribe anything on the 'start_thread' channel, as previously +recommended. Calling start once in the main thread should be sufficient +to start coverage on all threads. Calling start again in each thread +effectively clears any coverage data gathered up to that point. + +Run your code, then use the ``covercp.serve()`` function to browse the +results in a web browser. If you run this module from the command line, +it will call ``serve()`` for you. +""" + +import re +import sys +import cgi +from cherrypy._cpcompat import quote_plus +import os, os.path +localFile = os.path.join(os.path.dirname(__file__), "coverage.cache") + +the_coverage = None +try: + from coverage import coverage + the_coverage = coverage(data_file=localFile) + def start(): + the_coverage.start() +except ImportError: + # Setting the_coverage to None will raise errors + # that need to be trapped downstream. + the_coverage = None + + import warnings + warnings.warn("No code coverage will be performed; coverage.py could not be imported.") + + def start(): + pass +start.priority = 20 + +TEMPLATE_MENU = """ + + CherryPy Coverage Menu + + + +

CherryPy Coverage

""" + +TEMPLATE_FORM = """ +
+
+ + Show percentages
+ Hide files over %%
+ Exclude files matching
+ +
+ + + +
""" + +TEMPLATE_FRAMESET = """ +CherryPy coverage data + + + + + +""" + +TEMPLATE_COVERAGE = """ + + Coverage for %(name)s + + + +

%(name)s

+

%(fullpath)s

+

Coverage: %(pc)s%%

""" + +TEMPLATE_LOC_COVERED = """
+ + +\n""" +TEMPLATE_LOC_NOT_COVERED = """ + + +\n""" +TEMPLATE_LOC_EXCLUDED = """ + + +\n""" + +TEMPLATE_ITEM = "%s%s%s\n" + +def _percent(statements, missing): + s = len(statements) + e = s - len(missing) + if s > 0: + return int(round(100.0 * e / s)) + return 0 + +def _show_branch(root, base, path, pct=0, showpct=False, exclude="", + coverage=the_coverage): + + # Show the directory name and any of our children + dirs = [k for k, v in root.items() if v] + dirs.sort() + for name in dirs: + newpath = os.path.join(path, name) + + if newpath.lower().startswith(base): + relpath = newpath[len(base):] + yield "| " * relpath.count(os.sep) + yield "%s\n" % \ + (newpath, quote_plus(exclude), name) + + for chunk in _show_branch(root[name], base, newpath, pct, showpct, exclude, coverage=coverage): + yield chunk + + # Now list the files + if path.lower().startswith(base): + relpath = path[len(base):] + files = [k for k, v in root.items() if not v] + files.sort() + for name in files: + newpath = os.path.join(path, name) + + pc_str = "" + if showpct: + try: + _, statements, _, missing, _ = coverage.analysis2(newpath) + except: + # Yes, we really want to pass on all errors. + pass + else: + pc = _percent(statements, missing) + pc_str = ("%3d%% " % pc).replace(' ',' ') + if pc < float(pct) or pc == -1: + pc_str = "%s" % pc_str + else: + pc_str = "%s" % pc_str + + yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1), + pc_str, newpath, name) + +def _skip_file(path, exclude): + if exclude: + return bool(re.search(exclude, path)) + +def _graft(path, tree): + d = tree + + p = path + atoms = [] + while True: + p, tail = os.path.split(p) + if not tail: + break + atoms.append(tail) + atoms.append(p) + if p != "/": + atoms.append("/") + + atoms.reverse() + for node in atoms: + if node: + d = d.setdefault(node, {}) + +def get_tree(base, exclude, coverage=the_coverage): + """Return covered module names as a nested dict.""" + tree = {} + runs = coverage.data.executed_files() + for path in runs: + if not _skip_file(path, exclude) and not os.path.isdir(path): + _graft(path, tree) + return tree + +class CoverStats(object): + + def __init__(self, coverage, root=None): + self.coverage = coverage + if root is None: + # Guess initial depth. Files outside this path will not be + # reachable from the web interface. + import cherrypy + root = os.path.dirname(cherrypy.__file__) + self.root = root + + def index(self): + return TEMPLATE_FRAMESET % self.root.lower() + index.exposed = True + + def menu(self, base="/", pct="50", showpct="", + exclude=r'python\d\.\d|test|tut\d|tutorial'): + + # The coverage module uses all-lower-case names. + base = base.lower().rstrip(os.sep) + + yield TEMPLATE_MENU + yield TEMPLATE_FORM % locals() + + # Start by showing links for parent paths + yield "
" + path = "" + atoms = base.split(os.sep) + atoms.pop() + for atom in atoms: + path += atom + os.sep + yield ("%s %s" + % (path, quote_plus(exclude), atom, os.sep)) + yield "
" + + yield "
" + + # Then display the tree + tree = get_tree(base, exclude, self.coverage) + if not tree: + yield "

No modules covered.

" + else: + for chunk in _show_branch(tree, base, "/", pct, + showpct=='checked', exclude, coverage=self.coverage): + yield chunk + + yield "
" + yield "" + menu.exposed = True + + def annotated_file(self, filename, statements, excluded, missing): + source = open(filename, 'r') + buffer = [] + for lineno, line in enumerate(source.readlines()): + lineno += 1 + line = line.strip("\n\r") + empty_the_buffer = True + if lineno in excluded: + template = TEMPLATE_LOC_EXCLUDED + elif lineno in missing: + template = TEMPLATE_LOC_NOT_COVERED + elif lineno in statements: + template = TEMPLATE_LOC_COVERED + else: + empty_the_buffer = False + buffer.append((lineno, line)) + if empty_the_buffer: + for lno, pastline in buffer: + yield template % (lno, cgi.escape(pastline)) + buffer = [] + yield template % (lineno, cgi.escape(line)) + + def report(self, name): + filename, statements, excluded, missing, _ = self.coverage.analysis2(name) + pc = _percent(statements, missing) + yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name), + fullpath=name, + pc=pc) + yield '
%s %s
%s %s
%s %s
\n' + for line in self.annotated_file(filename, statements, excluded, + missing): + yield line + yield '
' + yield '' + yield '' + report.exposed = True + + +def serve(path=localFile, port=8080, root=None): + if coverage is None: + raise ImportError("The coverage module could not be imported.") + from coverage import coverage + cov = coverage(data_file = path) + cov.load() + + import cherrypy + cherrypy.config.update({'server.socket_port': int(port), + 'server.thread_pool': 10, + 'environment': "production", + }) + cherrypy.quickstart(CoverStats(cov, root)) + +if __name__ == "__main__": + serve(*tuple(sys.argv[1:])) + diff --git a/libs/cherrypy/lib/cpstats.py b/libs/cherrypy/lib/cpstats.py new file mode 100644 index 0000000..9be947f --- /dev/null +++ b/libs/cherrypy/lib/cpstats.py @@ -0,0 +1,662 @@ +"""CPStats, a package for collecting and reporting on program statistics. + +Overview +======== + +Statistics about program operation are an invaluable monitoring and debugging +tool. Unfortunately, the gathering and reporting of these critical values is +usually ad-hoc. This package aims to add a centralized place for gathering +statistical performance data, a structure for recording that data which +provides for extrapolation of that data into more useful information, +and a method of serving that data to both human investigators and +monitoring software. Let's examine each of those in more detail. + +Data Gathering +-------------- + +Just as Python's `logging` module provides a common importable for gathering +and sending messages, performance statistics would benefit from a similar +common mechanism, and one that does *not* require each package which wishes +to collect stats to import a third-party module. Therefore, we choose to +re-use the `logging` module by adding a `statistics` object to it. + +That `logging.statistics` object is a nested dict. It is not a custom class, +because that would 1) require libraries and applications to import a third- +party module in order to participate, 2) inhibit innovation in extrapolation +approaches and in reporting tools, and 3) be slow. There are, however, some +specifications regarding the structure of the dict. + + { + +----"SQLAlchemy": { + | "Inserts": 4389745, + | "Inserts per Second": + | lambda s: s["Inserts"] / (time() - s["Start"]), + | C +---"Table Statistics": { + | o | "widgets": {-----------+ + N | l | "Rows": 1.3M, | Record + a | l | "Inserts": 400, | + m | e | },---------------------+ + e | c | "froobles": { + s | t | "Rows": 7845, + p | i | "Inserts": 0, + a | o | }, + c | n +---}, + e | "Slow Queries": + | [{"Query": "SELECT * FROM widgets;", + | "Processing Time": 47.840923343, + | }, + | ], + +----}, + } + +The `logging.statistics` dict has four levels. The topmost level is nothing +more than a set of names to introduce modularity, usually along the lines of +package names. If the SQLAlchemy project wanted to participate, for example, +it might populate the item `logging.statistics['SQLAlchemy']`, whose value +would be a second-layer dict we call a "namespace". Namespaces help multiple +packages to avoid collisions over key names, and make reports easier to read, +to boot. The maintainers of SQLAlchemy should feel free to use more than one +namespace if needed (such as 'SQLAlchemy ORM'). Note that there are no case +or other syntax constraints on the namespace names; they should be chosen +to be maximally readable by humans (neither too short nor too long). + +Each namespace, then, is a dict of named statistical values, such as +'Requests/sec' or 'Uptime'. You should choose names which will look +good on a report: spaces and capitalization are just fine. + +In addition to scalars, values in a namespace MAY be a (third-layer) +dict, or a list, called a "collection". For example, the CherryPy StatsTool +keeps track of what each request is doing (or has most recently done) +in a 'Requests' collection, where each key is a thread ID; each +value in the subdict MUST be a fourth dict (whew!) of statistical data about +each thread. We call each subdict in the collection a "record". Similarly, +the StatsTool also keeps a list of slow queries, where each record contains +data about each slow query, in order. + +Values in a namespace or record may also be functions, which brings us to: + +Extrapolation +------------- + +The collection of statistical data needs to be fast, as close to unnoticeable +as possible to the host program. That requires us to minimize I/O, for example, +but in Python it also means we need to minimize function calls. So when you +are designing your namespace and record values, try to insert the most basic +scalar values you already have on hand. + +When it comes time to report on the gathered data, however, we usually have +much more freedom in what we can calculate. Therefore, whenever reporting +tools (like the provided StatsPage CherryPy class) fetch the contents of +`logging.statistics` for reporting, they first call `extrapolate_statistics` +(passing the whole `statistics` dict as the only argument). This makes a +deep copy of the statistics dict so that the reporting tool can both iterate +over it and even change it without harming the original. But it also expands +any functions in the dict by calling them. For example, you might have a +'Current Time' entry in the namespace with the value "lambda scope: time.time()". +The "scope" parameter is the current namespace dict (or record, if we're +currently expanding one of those instead), allowing you access to existing +static entries. If you're truly evil, you can even modify more than one entry +at a time. + +However, don't try to calculate an entry and then use its value in further +extrapolations; the order in which the functions are called is not guaranteed. +This can lead to a certain amount of duplicated work (or a redesign of your +schema), but that's better than complicating the spec. + +After the whole thing has been extrapolated, it's time for: + +Reporting +--------- + +The StatsPage class grabs the `logging.statistics` dict, extrapolates it all, +and then transforms it to HTML for easy viewing. Each namespace gets its own +header and attribute table, plus an extra table for each collection. This is +NOT part of the statistics specification; other tools can format how they like. + +You can control which columns are output and how they are formatted by updating +StatsPage.formatting, which is a dict that mirrors the keys and nesting of +`logging.statistics`. The difference is that, instead of data values, it has +formatting values. Use None for a given key to indicate to the StatsPage that a +given column should not be output. Use a string with formatting (such as '%.3f') +to interpolate the value(s), or use a callable (such as lambda v: v.isoformat()) +for more advanced formatting. Any entry which is not mentioned in the formatting +dict is output unchanged. + +Monitoring +---------- + +Although the HTML output takes pains to assign unique id's to each with +statistical data, you're probably better off fetching /cpstats/data, which +outputs the whole (extrapolated) `logging.statistics` dict in JSON format. +That is probably easier to parse, and doesn't have any formatting controls, +so you get the "original" data in a consistently-serialized format. +Note: there's no treatment yet for datetime objects. Try time.time() instead +for now if you can. Nagios will probably thank you. + +Turning Collection Off +---------------------- + +It is recommended each namespace have an "Enabled" item which, if False, +stops collection (but not reporting) of statistical data. Applications +SHOULD provide controls to pause and resume collection by setting these +entries to False or True, if present. + + +Usage +===== + +To collect statistics on CherryPy applications: + + from cherrypy.lib import cpstats + appconfig['/']['tools.cpstats.on'] = True + +To collect statistics on your own code: + + import logging + # Initialize the repository + if not hasattr(logging, 'statistics'): logging.statistics = {} + # Initialize my namespace + mystats = logging.statistics.setdefault('My Stuff', {}) + # Initialize my namespace's scalars and collections + mystats.update({ + 'Enabled': True, + 'Start Time': time.time(), + 'Important Events': 0, + 'Events/Second': lambda s: ( + (s['Important Events'] / (time.time() - s['Start Time']))), + }) + ... + for event in events: + ... + # Collect stats + if mystats.get('Enabled', False): + mystats['Important Events'] += 1 + +To report statistics: + + root.cpstats = cpstats.StatsPage() + +To format statistics reports: + + See 'Reporting', above. + +""" + +# -------------------------------- Statistics -------------------------------- # + +import logging +if not hasattr(logging, 'statistics'): logging.statistics = {} + +def extrapolate_statistics(scope): + """Return an extrapolated copy of the given scope.""" + c = {} + for k, v in list(scope.items()): + if isinstance(v, dict): + v = extrapolate_statistics(v) + elif isinstance(v, (list, tuple)): + v = [extrapolate_statistics(record) for record in v] + elif hasattr(v, '__call__'): + v = v(scope) + c[k] = v + return c + + +# --------------------- CherryPy Applications Statistics --------------------- # + +import threading +import time + +import cherrypy + +appstats = logging.statistics.setdefault('CherryPy Applications', {}) +appstats.update({ + 'Enabled': True, + 'Bytes Read/Request': lambda s: (s['Total Requests'] and + (s['Total Bytes Read'] / float(s['Total Requests'])) or 0.0), + 'Bytes Read/Second': lambda s: s['Total Bytes Read'] / s['Uptime'](s), + 'Bytes Written/Request': lambda s: (s['Total Requests'] and + (s['Total Bytes Written'] / float(s['Total Requests'])) or 0.0), + 'Bytes Written/Second': lambda s: s['Total Bytes Written'] / s['Uptime'](s), + 'Current Time': lambda s: time.time(), + 'Current Requests': 0, + 'Requests/Second': lambda s: float(s['Total Requests']) / s['Uptime'](s), + 'Server Version': cherrypy.__version__, + 'Start Time': time.time(), + 'Total Bytes Read': 0, + 'Total Bytes Written': 0, + 'Total Requests': 0, + 'Total Time': 0, + 'Uptime': lambda s: time.time() - s['Start Time'], + 'Requests': {}, + }) + +proc_time = lambda s: time.time() - s['Start Time'] + + +class ByteCountWrapper(object): + """Wraps a file-like object, counting the number of bytes read.""" + + def __init__(self, rfile): + self.rfile = rfile + self.bytes_read = 0 + + def read(self, size=-1): + data = self.rfile.read(size) + self.bytes_read += len(data) + return data + + def readline(self, size=-1): + data = self.rfile.readline(size) + self.bytes_read += len(data) + return data + + def readlines(self, sizehint=0): + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline() + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline() + return lines + + def close(self): + self.rfile.close() + + def __iter__(self): + return self + + def next(self): + data = self.rfile.next() + self.bytes_read += len(data) + return data + + +average_uriset_time = lambda s: s['Count'] and (s['Sum'] / s['Count']) or 0 + + +class StatsTool(cherrypy.Tool): + """Record various information about the current request.""" + + def __init__(self): + cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop) + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + if appstats.get('Enabled', False): + cherrypy.Tool._setup(self) + self.record_start() + + def record_start(self): + """Record the beginning of a request.""" + request = cherrypy.serving.request + if not hasattr(request.rfile, 'bytes_read'): + request.rfile = ByteCountWrapper(request.rfile) + request.body.fp = request.rfile + + r = request.remote + + appstats['Current Requests'] += 1 + appstats['Total Requests'] += 1 + appstats['Requests'][threading._get_ident()] = { + 'Bytes Read': None, + 'Bytes Written': None, + # Use a lambda so the ip gets updated by tools.proxy later + 'Client': lambda s: '%s:%s' % (r.ip, r.port), + 'End Time': None, + 'Processing Time': proc_time, + 'Request-Line': request.request_line, + 'Response Status': None, + 'Start Time': time.time(), + } + + def record_stop(self, uriset=None, slow_queries=1.0, slow_queries_count=100, + debug=False, **kwargs): + """Record the end of a request.""" + resp = cherrypy.serving.response + w = appstats['Requests'][threading._get_ident()] + + r = cherrypy.request.rfile.bytes_read + w['Bytes Read'] = r + appstats['Total Bytes Read'] += r + + if resp.stream: + w['Bytes Written'] = 'chunked' + else: + cl = int(resp.headers.get('Content-Length', 0)) + w['Bytes Written'] = cl + appstats['Total Bytes Written'] += cl + + w['Response Status'] = getattr(resp, 'output_status', None) or resp.status + + w['End Time'] = time.time() + p = w['End Time'] - w['Start Time'] + w['Processing Time'] = p + appstats['Total Time'] += p + + appstats['Current Requests'] -= 1 + + if debug: + cherrypy.log('Stats recorded: %s' % repr(w), 'TOOLS.CPSTATS') + + if uriset: + rs = appstats.setdefault('URI Set Tracking', {}) + r = rs.setdefault(uriset, { + 'Min': None, 'Max': None, 'Count': 0, 'Sum': 0, + 'Avg': average_uriset_time}) + if r['Min'] is None or p < r['Min']: + r['Min'] = p + if r['Max'] is None or p > r['Max']: + r['Max'] = p + r['Count'] += 1 + r['Sum'] += p + + if slow_queries and p > slow_queries: + sq = appstats.setdefault('Slow Queries', []) + sq.append(w.copy()) + if len(sq) > slow_queries_count: + sq.pop(0) + + +import cherrypy +cherrypy.tools.cpstats = StatsTool() + + +# ---------------------- CherryPy Statistics Reporting ---------------------- # + +import os +thisdir = os.path.abspath(os.path.dirname(__file__)) + +try: + import json +except ImportError: + try: + import simplejson as json + except ImportError: + json = None + + +missing = object() + +locale_date = lambda v: time.strftime('%c', time.gmtime(v)) +iso_format = lambda v: time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v)) + +def pause_resume(ns): + def _pause_resume(enabled): + pause_disabled = '' + resume_disabled = '' + if enabled: + resume_disabled = 'disabled="disabled" ' + else: + pause_disabled = 'disabled="disabled" ' + return """ +
+ + +
+
+ + +
+ """ % (ns, pause_disabled, ns, resume_disabled) + return _pause_resume + + +class StatsPage(object): + + formatting = { + 'CherryPy Applications': { + 'Enabled': pause_resume('CherryPy Applications'), + 'Bytes Read/Request': '%.3f', + 'Bytes Read/Second': '%.3f', + 'Bytes Written/Request': '%.3f', + 'Bytes Written/Second': '%.3f', + 'Current Time': iso_format, + 'Requests/Second': '%.3f', + 'Start Time': iso_format, + 'Total Time': '%.3f', + 'Uptime': '%.3f', + 'Slow Queries': { + 'End Time': None, + 'Processing Time': '%.3f', + 'Start Time': iso_format, + }, + 'URI Set Tracking': { + 'Avg': '%.3f', + 'Max': '%.3f', + 'Min': '%.3f', + 'Sum': '%.3f', + }, + 'Requests': { + 'Bytes Read': '%s', + 'Bytes Written': '%s', + 'End Time': None, + 'Processing Time': '%.3f', + 'Start Time': None, + }, + }, + 'CherryPy WSGIServer': { + 'Enabled': pause_resume('CherryPy WSGIServer'), + 'Connections/second': '%.3f', + 'Start time': iso_format, + }, + } + + + def index(self): + # Transform the raw data into pretty output for HTML + yield """ + + + Statistics + + + +""" + for title, scalars, collections in self.get_namespaces(): + yield """ +

%s

+ + + +""" % title + for i, (key, value) in enumerate(scalars): + colnum = i % 3 + if colnum == 0: yield """ + """ + yield """ + """ % vars() + if colnum == 2: yield """ + """ + + if colnum == 0: yield """ + + + """ + elif colnum == 1: yield """ + + """ + yield """ + +
%(key)s%(value)s
""" + + for subtitle, headers, subrows in collections: + yield """ +

%s

+ + + """ % subtitle + for key in headers: + yield """ + """ % key + yield """ + + + """ + for subrow in subrows: + yield """ + """ + for value in subrow: + yield """ + """ % value + yield """ + """ + yield """ + +
%s
%s
""" + yield """ + + +""" + index.exposed = True + + def get_namespaces(self): + """Yield (title, scalars, collections) for each namespace.""" + s = extrapolate_statistics(logging.statistics) + for title, ns in sorted(s.items()): + scalars = [] + collections = [] + ns_fmt = self.formatting.get(title, {}) + for k, v in sorted(ns.items()): + fmt = ns_fmt.get(k, {}) + if isinstance(v, dict): + headers, subrows = self.get_dict_collection(v, fmt) + collections.append((k, ['ID'] + headers, subrows)) + elif isinstance(v, (list, tuple)): + headers, subrows = self.get_list_collection(v, fmt) + collections.append((k, headers, subrows)) + else: + format = ns_fmt.get(k, missing) + if format is None: + # Don't output this column. + continue + if hasattr(format, '__call__'): + v = format(v) + elif format is not missing: + v = format % v + scalars.append((k, v)) + yield title, scalars, collections + + def get_dict_collection(self, v, formatting): + """Return ([headers], [rows]) for the given collection.""" + # E.g., the 'Requests' dict. + headers = [] + for record in v.itervalues(): + for k3 in record: + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if k3 not in headers: + headers.append(k3) + headers.sort() + + subrows = [] + for k2, record in sorted(v.items()): + subrow = [k2] + for k3 in headers: + v3 = record.get(k3, '') + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if hasattr(format, '__call__'): + v3 = format(v3) + elif format is not missing: + v3 = format % v3 + subrow.append(v3) + subrows.append(subrow) + + return headers, subrows + + def get_list_collection(self, v, formatting): + """Return ([headers], [subrows]) for the given collection.""" + # E.g., the 'Slow Queries' list. + headers = [] + for record in v: + for k3 in record: + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if k3 not in headers: + headers.append(k3) + headers.sort() + + subrows = [] + for record in v: + subrow = [] + for k3 in headers: + v3 = record.get(k3, '') + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if hasattr(format, '__call__'): + v3 = format(v3) + elif format is not missing: + v3 = format % v3 + subrow.append(v3) + subrows.append(subrow) + + return headers, subrows + + if json is not None: + def data(self): + s = extrapolate_statistics(logging.statistics) + cherrypy.response.headers['Content-Type'] = 'application/json' + return json.dumps(s, sort_keys=True, indent=4) + data.exposed = True + + def pause(self, namespace): + logging.statistics.get(namespace, {})['Enabled'] = False + raise cherrypy.HTTPRedirect('./') + pause.exposed = True + pause.cp_config = {'tools.allow.on': True, + 'tools.allow.methods': ['POST']} + + def resume(self, namespace): + logging.statistics.get(namespace, {})['Enabled'] = True + raise cherrypy.HTTPRedirect('./') + resume.exposed = True + resume.cp_config = {'tools.allow.on': True, + 'tools.allow.methods': ['POST']} + diff --git a/libs/cherrypy/lib/cptools.py b/libs/cherrypy/lib/cptools.py new file mode 100644 index 0000000..b426a3e --- /dev/null +++ b/libs/cherrypy/lib/cptools.py @@ -0,0 +1,617 @@ +"""Functions for builtin CherryPy tools.""" + +import logging +import re + +import cherrypy +from cherrypy._cpcompat import basestring, ntob, md5, set +from cherrypy.lib import httputil as _httputil + + +# Conditional HTTP request support # + +def validate_etags(autotags=False, debug=False): + """Validate the current ETag against If-Match, If-None-Match headers. + + If autotags is True, an ETag response-header value will be provided + from an MD5 hash of the response body (unless some other code has + already provided an ETag header). If False (the default), the ETag + will not be automatic. + + WARNING: the autotags feature is not designed for URL's which allow + methods other than GET. For example, if a POST to the same URL returns + no content, the automatic ETag will be incorrect, breaking a fundamental + use for entity tags in a possibly destructive fashion. Likewise, if you + raise 304 Not Modified, the response body will be empty, the ETag hash + will be incorrect, and your application will break. + See :rfc:`2616` Section 14.24. + """ + response = cherrypy.serving.response + + # Guard against being run twice. + if hasattr(response, "ETag"): + return + + status, reason, msg = _httputil.valid_status(response.status) + + etag = response.headers.get('ETag') + + # Automatic ETag generation. See warning in docstring. + if etag: + if debug: + cherrypy.log('ETag already set: %s' % etag, 'TOOLS.ETAGS') + elif not autotags: + if debug: + cherrypy.log('Autotags off', 'TOOLS.ETAGS') + elif status != 200: + if debug: + cherrypy.log('Status not 200', 'TOOLS.ETAGS') + else: + etag = response.collapse_body() + etag = '"%s"' % md5(etag).hexdigest() + if debug: + cherrypy.log('Setting ETag: %s' % etag, 'TOOLS.ETAGS') + response.headers['ETag'] = etag + + response.ETag = etag + + # "If the request would, without the If-Match header field, result in + # anything other than a 2xx or 412 status, then the If-Match header + # MUST be ignored." + if debug: + cherrypy.log('Status: %s' % status, 'TOOLS.ETAGS') + if status >= 200 and status <= 299: + request = cherrypy.serving.request + + conditions = request.headers.elements('If-Match') or [] + conditions = [str(x) for x in conditions] + if debug: + cherrypy.log('If-Match conditions: %s' % repr(conditions), + 'TOOLS.ETAGS') + if conditions and not (conditions == ["*"] or etag in conditions): + raise cherrypy.HTTPError(412, "If-Match failed: ETag %r did " + "not match %r" % (etag, conditions)) + + conditions = request.headers.elements('If-None-Match') or [] + conditions = [str(x) for x in conditions] + if debug: + cherrypy.log('If-None-Match conditions: %s' % repr(conditions), + 'TOOLS.ETAGS') + if conditions == ["*"] or etag in conditions: + if debug: + cherrypy.log('request.method: %s' % request.method, 'TOOLS.ETAGS') + if request.method in ("GET", "HEAD"): + raise cherrypy.HTTPRedirect([], 304) + else: + raise cherrypy.HTTPError(412, "If-None-Match failed: ETag %r " + "matched %r" % (etag, conditions)) + +def validate_since(): + """Validate the current Last-Modified against If-Modified-Since headers. + + If no code has set the Last-Modified response header, then no validation + will be performed. + """ + response = cherrypy.serving.response + lastmod = response.headers.get('Last-Modified') + if lastmod: + status, reason, msg = _httputil.valid_status(response.status) + + request = cherrypy.serving.request + + since = request.headers.get('If-Unmodified-Since') + if since and since != lastmod: + if (status >= 200 and status <= 299) or status == 412: + raise cherrypy.HTTPError(412) + + since = request.headers.get('If-Modified-Since') + if since and since == lastmod: + if (status >= 200 and status <= 299) or status == 304: + if request.method in ("GET", "HEAD"): + raise cherrypy.HTTPRedirect([], 304) + else: + raise cherrypy.HTTPError(412) + + +# Tool code # + +def allow(methods=None, debug=False): + """Raise 405 if request.method not in methods (default ['GET', 'HEAD']). + + The given methods are case-insensitive, and may be in any order. + If only one method is allowed, you may supply a single string; + if more than one, supply a list of strings. + + Regardless of whether the current method is allowed or not, this + also emits an 'Allow' response header, containing the given methods. + """ + if not isinstance(methods, (tuple, list)): + methods = [methods] + methods = [m.upper() for m in methods if m] + if not methods: + methods = ['GET', 'HEAD'] + elif 'GET' in methods and 'HEAD' not in methods: + methods.append('HEAD') + + cherrypy.response.headers['Allow'] = ', '.join(methods) + if cherrypy.request.method not in methods: + if debug: + cherrypy.log('request.method %r not in methods %r' % + (cherrypy.request.method, methods), 'TOOLS.ALLOW') + raise cherrypy.HTTPError(405) + else: + if debug: + cherrypy.log('request.method %r in methods %r' % + (cherrypy.request.method, methods), 'TOOLS.ALLOW') + + +def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For', + scheme='X-Forwarded-Proto', debug=False): + """Change the base URL (scheme://host[:port][/path]). + + For running a CP server behind Apache, lighttpd, or other HTTP server. + + For Apache and lighttpd, you should leave the 'local' argument at the + default value of 'X-Forwarded-Host'. For Squid, you probably want to set + tools.proxy.local = 'Origin'. + + If you want the new request.base to include path info (not just the host), + you must explicitly set base to the full base path, and ALSO set 'local' + to '', so that the X-Forwarded-Host request header (which never includes + path info) does not override it. Regardless, the value for 'base' MUST + NOT end in a slash. + + cherrypy.request.remote.ip (the IP address of the client) will be + rewritten if the header specified by the 'remote' arg is valid. + By default, 'remote' is set to 'X-Forwarded-For'. If you do not + want to rewrite remote.ip, set the 'remote' arg to an empty string. + """ + + request = cherrypy.serving.request + + if scheme: + s = request.headers.get(scheme, None) + if debug: + cherrypy.log('Testing scheme %r:%r' % (scheme, s), 'TOOLS.PROXY') + if s == 'on' and 'ssl' in scheme.lower(): + # This handles e.g. webfaction's 'X-Forwarded-Ssl: on' header + scheme = 'https' + else: + # This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https' + scheme = s + if not scheme: + scheme = request.base[:request.base.find("://")] + + if local: + lbase = request.headers.get(local, None) + if debug: + cherrypy.log('Testing local %r:%r' % (local, lbase), 'TOOLS.PROXY') + if lbase is not None: + base = lbase.split(',')[0] + if not base: + port = request.local.port + if port == 80: + base = '127.0.0.1' + else: + base = '127.0.0.1:%s' % port + + if base.find("://") == -1: + # add http:// or https:// if needed + base = scheme + "://" + base + + request.base = base + + if remote: + xff = request.headers.get(remote) + if debug: + cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY') + if xff: + if remote == 'X-Forwarded-For': + # See http://bob.pythonmac.org/archives/2005/09/23/apache-x-forwarded-for-caveat/ + xff = xff.split(',')[-1].strip() + request.remote.ip = xff + + +def ignore_headers(headers=('Range',), debug=False): + """Delete request headers whose field names are included in 'headers'. + + This is a useful tool for working behind certain HTTP servers; + for example, Apache duplicates the work that CP does for 'Range' + headers, and will doubly-truncate the response. + """ + request = cherrypy.serving.request + for name in headers: + if name in request.headers: + if debug: + cherrypy.log('Ignoring request header %r' % name, + 'TOOLS.IGNORE_HEADERS') + del request.headers[name] + + +def response_headers(headers=None, debug=False): + """Set headers on the response.""" + if debug: + cherrypy.log('Setting response headers: %s' % repr(headers), + 'TOOLS.RESPONSE_HEADERS') + for name, value in (headers or []): + cherrypy.serving.response.headers[name] = value +response_headers.failsafe = True + + +def referer(pattern, accept=True, accept_missing=False, error=403, + message='Forbidden Referer header.', debug=False): + """Raise HTTPError if Referer header does/does not match the given pattern. + + pattern + A regular expression pattern to test against the Referer. + + accept + If True, the Referer must match the pattern; if False, + the Referer must NOT match the pattern. + + accept_missing + If True, permit requests with no Referer header. + + error + The HTTP error code to return to the client on failure. + + message + A string to include in the response body on failure. + + """ + try: + ref = cherrypy.serving.request.headers['Referer'] + match = bool(re.match(pattern, ref)) + if debug: + cherrypy.log('Referer %r matches %r' % (ref, pattern), + 'TOOLS.REFERER') + if accept == match: + return + except KeyError: + if debug: + cherrypy.log('No Referer header', 'TOOLS.REFERER') + if accept_missing: + return + + raise cherrypy.HTTPError(error, message) + + +class SessionAuth(object): + """Assert that the user is logged in.""" + + session_key = "username" + debug = False + + def check_username_and_password(self, username, password): + pass + + def anonymous(self): + """Provide a temporary user name for anonymous users.""" + pass + + def on_login(self, username): + pass + + def on_logout(self, username): + pass + + def on_check(self, username): + pass + + def login_screen(self, from_page='..', username='', error_msg='', **kwargs): + return ntob(""" +Message: %(error_msg)s +
+ Login:
+ Password:
+
+ +
+""" % {'from_page': from_page, 'username': username, + 'error_msg': error_msg}, "utf-8") + + def do_login(self, username, password, from_page='..', **kwargs): + """Login. May raise redirect, or return True if request handled.""" + response = cherrypy.serving.response + error_msg = self.check_username_and_password(username, password) + if error_msg: + body = self.login_screen(from_page, username, error_msg) + response.body = body + if "Content-Length" in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers["Content-Length"] + return True + else: + cherrypy.serving.request.login = username + cherrypy.session[self.session_key] = username + self.on_login(username) + raise cherrypy.HTTPRedirect(from_page or "/") + + def do_logout(self, from_page='..', **kwargs): + """Logout. May raise redirect, or return True if request handled.""" + sess = cherrypy.session + username = sess.get(self.session_key) + sess[self.session_key] = None + if username: + cherrypy.serving.request.login = None + self.on_logout(username) + raise cherrypy.HTTPRedirect(from_page) + + def do_check(self): + """Assert username. May raise redirect, or return True if request handled.""" + sess = cherrypy.session + request = cherrypy.serving.request + response = cherrypy.serving.response + + username = sess.get(self.session_key) + if not username: + sess[self.session_key] = username = self.anonymous() + if self.debug: + cherrypy.log('No session[username], trying anonymous', 'TOOLS.SESSAUTH') + if not username: + url = cherrypy.url(qs=request.query_string) + if self.debug: + cherrypy.log('No username, routing to login_screen with ' + 'from_page %r' % url, 'TOOLS.SESSAUTH') + response.body = self.login_screen(url) + if "Content-Length" in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers["Content-Length"] + return True + if self.debug: + cherrypy.log('Setting request.login to %r' % username, 'TOOLS.SESSAUTH') + request.login = username + self.on_check(username) + + def run(self): + request = cherrypy.serving.request + response = cherrypy.serving.response + + path = request.path_info + if path.endswith('login_screen'): + if self.debug: + cherrypy.log('routing %r to login_screen' % path, 'TOOLS.SESSAUTH') + return self.login_screen(**request.params) + elif path.endswith('do_login'): + if request.method != 'POST': + response.headers['Allow'] = "POST" + if self.debug: + cherrypy.log('do_login requires POST', 'TOOLS.SESSAUTH') + raise cherrypy.HTTPError(405) + if self.debug: + cherrypy.log('routing %r to do_login' % path, 'TOOLS.SESSAUTH') + return self.do_login(**request.params) + elif path.endswith('do_logout'): + if request.method != 'POST': + response.headers['Allow'] = "POST" + raise cherrypy.HTTPError(405) + if self.debug: + cherrypy.log('routing %r to do_logout' % path, 'TOOLS.SESSAUTH') + return self.do_logout(**request.params) + else: + if self.debug: + cherrypy.log('No special path, running do_check', 'TOOLS.SESSAUTH') + return self.do_check() + + +def session_auth(**kwargs): + sa = SessionAuth() + for k, v in kwargs.items(): + setattr(sa, k, v) + return sa.run() +session_auth.__doc__ = """Session authentication hook. + +Any attribute of the SessionAuth class may be overridden via a keyword arg +to this function: + +""" + "\n".join(["%s: %s" % (k, type(getattr(SessionAuth, k)).__name__) + for k in dir(SessionAuth) if not k.startswith("__")]) + + +def log_traceback(severity=logging.ERROR, debug=False): + """Write the last error's traceback to the cherrypy error log.""" + cherrypy.log("", "HTTP", severity=severity, traceback=True) + +def log_request_headers(debug=False): + """Write request headers to the cherrypy error log.""" + h = [" %s: %s" % (k, v) for k, v in cherrypy.serving.request.header_list] + cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), "HTTP") + +def log_hooks(debug=False): + """Write request.hooks to the cherrypy error log.""" + request = cherrypy.serving.request + + msg = [] + # Sort by the standard points if possible. + from cherrypy import _cprequest + points = _cprequest.hookpoints + for k in request.hooks.keys(): + if k not in points: + points.append(k) + + for k in points: + msg.append(" %s:" % k) + v = request.hooks.get(k, []) + v.sort() + for h in v: + msg.append(" %r" % h) + cherrypy.log('\nRequest Hooks for ' + cherrypy.url() + + ':\n' + '\n'.join(msg), "HTTP") + +def redirect(url='', internal=True, debug=False): + """Raise InternalRedirect or HTTPRedirect to the given url.""" + if debug: + cherrypy.log('Redirecting %sto: %s' % + ({True: 'internal ', False: ''}[internal], url), + 'TOOLS.REDIRECT') + if internal: + raise cherrypy.InternalRedirect(url) + else: + raise cherrypy.HTTPRedirect(url) + +def trailing_slash(missing=True, extra=False, status=None, debug=False): + """Redirect if path_info has (missing|extra) trailing slash.""" + request = cherrypy.serving.request + pi = request.path_info + + if debug: + cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' % + (request.is_index, missing, extra, pi), + 'TOOLS.TRAILING_SLASH') + if request.is_index is True: + if missing: + if not pi.endswith('/'): + new_url = cherrypy.url(pi + '/', request.query_string) + raise cherrypy.HTTPRedirect(new_url, status=status or 301) + elif request.is_index is False: + if extra: + # If pi == '/', don't redirect to ''! + if pi.endswith('/') and pi != '/': + new_url = cherrypy.url(pi[:-1], request.query_string) + raise cherrypy.HTTPRedirect(new_url, status=status or 301) + +def flatten(debug=False): + """Wrap response.body in a generator that recursively iterates over body. + + This allows cherrypy.response.body to consist of 'nested generators'; + that is, a set of generators that yield generators. + """ + import types + def flattener(input): + numchunks = 0 + for x in input: + if not isinstance(x, types.GeneratorType): + numchunks += 1 + yield x + else: + for y in flattener(x): + numchunks += 1 + yield y + if debug: + cherrypy.log('Flattened %d chunks' % numchunks, 'TOOLS.FLATTEN') + response = cherrypy.serving.response + response.body = flattener(response.body) + + +def accept(media=None, debug=False): + """Return the client's preferred media-type (from the given Content-Types). + + If 'media' is None (the default), no test will be performed. + + If 'media' is provided, it should be the Content-Type value (as a string) + or values (as a list or tuple of strings) which the current resource + can emit. The client's acceptable media ranges (as declared in the + Accept request header) will be matched in order to these Content-Type + values; the first such string is returned. That is, the return value + will always be one of the strings provided in the 'media' arg (or None + if 'media' is None). + + If no match is found, then HTTPError 406 (Not Acceptable) is raised. + Note that most web browsers send */* as a (low-quality) acceptable + media range, which should match any Content-Type. In addition, "...if + no Accept header field is present, then it is assumed that the client + accepts all media types." + + Matching types are checked in order of client preference first, + and then in the order of the given 'media' values. + + Note that this function does not honor accept-params (other than "q"). + """ + if not media: + return + if isinstance(media, basestring): + media = [media] + request = cherrypy.serving.request + + # Parse the Accept request header, and try to match one + # of the requested media-ranges (in order of preference). + ranges = request.headers.elements('Accept') + if not ranges: + # Any media type is acceptable. + if debug: + cherrypy.log('No Accept header elements', 'TOOLS.ACCEPT') + return media[0] + else: + # Note that 'ranges' is sorted in order of preference + for element in ranges: + if element.qvalue > 0: + if element.value == "*/*": + # Matches any type or subtype + if debug: + cherrypy.log('Match due to */*', 'TOOLS.ACCEPT') + return media[0] + elif element.value.endswith("/*"): + # Matches any subtype + mtype = element.value[:-1] # Keep the slash + for m in media: + if m.startswith(mtype): + if debug: + cherrypy.log('Match due to %s' % element.value, + 'TOOLS.ACCEPT') + return m + else: + # Matches exact value + if element.value in media: + if debug: + cherrypy.log('Match due to %s' % element.value, + 'TOOLS.ACCEPT') + return element.value + + # No suitable media-range found. + ah = request.headers.get('Accept') + if ah is None: + msg = "Your client did not send an Accept header." + else: + msg = "Your client sent this Accept header: %s." % ah + msg += (" But this resource only emits these media types: %s." % + ", ".join(media)) + raise cherrypy.HTTPError(406, msg) + + +class MonitoredHeaderMap(_httputil.HeaderMap): + + def __init__(self): + self.accessed_headers = set() + + def __getitem__(self, key): + self.accessed_headers.add(key) + return _httputil.HeaderMap.__getitem__(self, key) + + def __contains__(self, key): + self.accessed_headers.add(key) + return _httputil.HeaderMap.__contains__(self, key) + + def get(self, key, default=None): + self.accessed_headers.add(key) + return _httputil.HeaderMap.get(self, key, default=default) + + if hasattr({}, 'has_key'): + # Python 2 + def has_key(self, key): + self.accessed_headers.add(key) + return _httputil.HeaderMap.has_key(self, key) + + +def autovary(ignore=None, debug=False): + """Auto-populate the Vary response header based on request.header access.""" + request = cherrypy.serving.request + + req_h = request.headers + request.headers = MonitoredHeaderMap() + request.headers.update(req_h) + if ignore is None: + ignore = set(['Content-Disposition', 'Content-Length', 'Content-Type']) + + def set_response_header(): + resp_h = cherrypy.serving.response.headers + v = set([e.value for e in resp_h.elements('Vary')]) + if debug: + cherrypy.log('Accessed headers: %s' % request.headers.accessed_headers, + 'TOOLS.AUTOVARY') + v = v.union(request.headers.accessed_headers) + v = v.difference(ignore) + v = list(v) + v.sort() + resp_h['Vary'] = ', '.join(v) + request.hooks.attach('before_finalize', set_response_header, 95) + diff --git a/libs/cherrypy/lib/encoding.py b/libs/cherrypy/lib/encoding.py new file mode 100644 index 0000000..6459746 --- /dev/null +++ b/libs/cherrypy/lib/encoding.py @@ -0,0 +1,388 @@ +import struct +import time + +import cherrypy +from cherrypy._cpcompat import basestring, BytesIO, ntob, set, unicodestr +from cherrypy.lib import file_generator +from cherrypy.lib import set_vary_header + + +def decode(encoding=None, default_encoding='utf-8'): + """Replace or extend the list of charsets used to decode a request entity. + + Either argument may be a single string or a list of strings. + + encoding + If not None, restricts the set of charsets attempted while decoding + a request entity to the given set (even if a different charset is given in + the Content-Type request header). + + default_encoding + Only in effect if the 'encoding' argument is not given. + If given, the set of charsets attempted while decoding a request entity is + *extended* with the given value(s). + + """ + body = cherrypy.request.body + if encoding is not None: + if not isinstance(encoding, list): + encoding = [encoding] + body.attempt_charsets = encoding + elif default_encoding: + if not isinstance(default_encoding, list): + default_encoding = [default_encoding] + body.attempt_charsets = body.attempt_charsets + default_encoding + + +class ResponseEncoder: + + default_encoding = 'utf-8' + failmsg = "Response body could not be encoded with %r." + encoding = None + errors = 'strict' + text_only = True + add_charset = True + debug = False + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + self.attempted_charsets = set() + request = cherrypy.serving.request + if request.handler is not None: + # Replace request.handler with self + if self.debug: + cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE') + self.oldhandler = request.handler + request.handler = self + + def encode_stream(self, encoding): + """Encode a streaming response body. + + Use a generator wrapper, and just pray it works as the stream is + being written out. + """ + if encoding in self.attempted_charsets: + return False + self.attempted_charsets.add(encoding) + + def encoder(body): + for chunk in body: + if isinstance(chunk, unicodestr): + chunk = chunk.encode(encoding, self.errors) + yield chunk + self.body = encoder(self.body) + return True + + def encode_string(self, encoding): + """Encode a buffered response body.""" + if encoding in self.attempted_charsets: + return False + self.attempted_charsets.add(encoding) + + try: + body = [] + for chunk in self.body: + if isinstance(chunk, unicodestr): + chunk = chunk.encode(encoding, self.errors) + body.append(chunk) + self.body = body + except (LookupError, UnicodeError): + return False + else: + return True + + def find_acceptable_charset(self): + request = cherrypy.serving.request + response = cherrypy.serving.response + + if self.debug: + cherrypy.log('response.stream %r' % response.stream, 'TOOLS.ENCODE') + if response.stream: + encoder = self.encode_stream + else: + encoder = self.encode_string + if "Content-Length" in response.headers: + # Delete Content-Length header so finalize() recalcs it. + # Encoded strings may be of different lengths from their + # unicode equivalents, and even from each other. For example: + # >>> t = u"\u7007\u3040" + # >>> len(t) + # 2 + # >>> len(t.encode("UTF-8")) + # 6 + # >>> len(t.encode("utf7")) + # 8 + del response.headers["Content-Length"] + + # Parse the Accept-Charset request header, and try to provide one + # of the requested charsets (in order of user preference). + encs = request.headers.elements('Accept-Charset') + charsets = [enc.value.lower() for enc in encs] + if self.debug: + cherrypy.log('charsets %s' % repr(charsets), 'TOOLS.ENCODE') + + if self.encoding is not None: + # If specified, force this encoding to be used, or fail. + encoding = self.encoding.lower() + if self.debug: + cherrypy.log('Specified encoding %r' % encoding, 'TOOLS.ENCODE') + if (not charsets) or "*" in charsets or encoding in charsets: + if self.debug: + cherrypy.log('Attempting encoding %r' % encoding, 'TOOLS.ENCODE') + if encoder(encoding): + return encoding + else: + if not encs: + if self.debug: + cherrypy.log('Attempting default encoding %r' % + self.default_encoding, 'TOOLS.ENCODE') + # Any character-set is acceptable. + if encoder(self.default_encoding): + return self.default_encoding + else: + raise cherrypy.HTTPError(500, self.failmsg % self.default_encoding) + else: + for element in encs: + if element.qvalue > 0: + if element.value == "*": + # Matches any charset. Try our default. + if self.debug: + cherrypy.log('Attempting default encoding due ' + 'to %r' % element, 'TOOLS.ENCODE') + if encoder(self.default_encoding): + return self.default_encoding + else: + encoding = element.value + if self.debug: + cherrypy.log('Attempting encoding %s (qvalue >' + '0)' % element, 'TOOLS.ENCODE') + if encoder(encoding): + return encoding + + if "*" not in charsets: + # If no "*" is present in an Accept-Charset field, then all + # character sets not explicitly mentioned get a quality + # value of 0, except for ISO-8859-1, which gets a quality + # value of 1 if not explicitly mentioned. + iso = 'iso-8859-1' + if iso not in charsets: + if self.debug: + cherrypy.log('Attempting ISO-8859-1 encoding', + 'TOOLS.ENCODE') + if encoder(iso): + return iso + + # No suitable encoding found. + ac = request.headers.get('Accept-Charset') + if ac is None: + msg = "Your client did not send an Accept-Charset header." + else: + msg = "Your client sent this Accept-Charset header: %s." % ac + msg += " We tried these charsets: %s." % ", ".join(self.attempted_charsets) + raise cherrypy.HTTPError(406, msg) + + def __call__(self, *args, **kwargs): + response = cherrypy.serving.response + self.body = self.oldhandler(*args, **kwargs) + + if isinstance(self.body, basestring): + # strings get wrapped in a list because iterating over a single + # item list is much faster than iterating over every character + # in a long string. + if self.body: + self.body = [self.body] + else: + # [''] doesn't evaluate to False, so replace it with []. + self.body = [] + elif hasattr(self.body, 'read'): + self.body = file_generator(self.body) + elif self.body is None: + self.body = [] + + ct = response.headers.elements("Content-Type") + if self.debug: + cherrypy.log('Content-Type: %r' % [str(h) for h in ct], 'TOOLS.ENCODE') + if ct: + ct = ct[0] + if self.text_only: + if ct.value.lower().startswith("text/"): + if self.debug: + cherrypy.log('Content-Type %s starts with "text/"' % ct, + 'TOOLS.ENCODE') + do_find = True + else: + if self.debug: + cherrypy.log('Not finding because Content-Type %s does ' + 'not start with "text/"' % ct, + 'TOOLS.ENCODE') + do_find = False + else: + if self.debug: + cherrypy.log('Finding because not text_only', 'TOOLS.ENCODE') + do_find = True + + if do_find: + # Set "charset=..." param on response Content-Type header + ct.params['charset'] = self.find_acceptable_charset() + if self.add_charset: + if self.debug: + cherrypy.log('Setting Content-Type %s' % ct, + 'TOOLS.ENCODE') + response.headers["Content-Type"] = str(ct) + + return self.body + +# GZIP + +def compress(body, compress_level): + """Compress 'body' at the given compress_level.""" + import zlib + + # See http://www.gzip.org/zlib/rfc-gzip.html + yield ntob('\x1f\x8b') # ID1 and ID2: gzip marker + yield ntob('\x08') # CM: compression method + yield ntob('\x00') # FLG: none set + # MTIME: 4 bytes + yield struct.pack(" 0 is present + * The 'identity' value is given with a qvalue > 0. + + """ + request = cherrypy.serving.request + response = cherrypy.serving.response + + set_vary_header(response, "Accept-Encoding") + + if not response.body: + # Response body is empty (might be a 304 for instance) + if debug: + cherrypy.log('No response body', context='TOOLS.GZIP') + return + + # If returning cached content (which should already have been gzipped), + # don't re-zip. + if getattr(request, "cached", False): + if debug: + cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP') + return + + acceptable = request.headers.elements('Accept-Encoding') + if not acceptable: + # If no Accept-Encoding field is present in a request, + # the server MAY assume that the client will accept any + # content coding. In this case, if "identity" is one of + # the available content-codings, then the server SHOULD use + # the "identity" content-coding, unless it has additional + # information that a different content-coding is meaningful + # to the client. + if debug: + cherrypy.log('No Accept-Encoding', context='TOOLS.GZIP') + return + + ct = response.headers.get('Content-Type', '').split(';')[0] + for coding in acceptable: + if coding.value == 'identity' and coding.qvalue != 0: + if debug: + cherrypy.log('Non-zero identity qvalue: %s' % coding, + context='TOOLS.GZIP') + return + if coding.value in ('gzip', 'x-gzip'): + if coding.qvalue == 0: + if debug: + cherrypy.log('Zero gzip qvalue: %s' % coding, + context='TOOLS.GZIP') + return + + if ct not in mime_types: + # If the list of provided mime-types contains tokens + # such as 'text/*' or 'application/*+xml', + # we go through them and find the most appropriate one + # based on the given content-type. + # The pattern matching is only caring about the most + # common cases, as stated above, and doesn't support + # for extra parameters. + found = False + if '/' in ct: + ct_media_type, ct_sub_type = ct.split('/') + for mime_type in mime_types: + if '/' in mime_type: + media_type, sub_type = mime_type.split('/') + if ct_media_type == media_type: + if sub_type == '*': + found = True + break + elif '+' in sub_type and '+' in ct_sub_type: + ct_left, ct_right = ct_sub_type.split('+') + left, right = sub_type.split('+') + if left == '*' and ct_right == right: + found = True + break + + if not found: + if debug: + cherrypy.log('Content-Type %s not in mime_types %r' % + (ct, mime_types), context='TOOLS.GZIP') + return + + if debug: + cherrypy.log('Gzipping', context='TOOLS.GZIP') + # Return a generator that compresses the page + response.headers['Content-Encoding'] = 'gzip' + response.body = compress(response.body, compress_level) + if "Content-Length" in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers["Content-Length"] + + return + + if debug: + cherrypy.log('No acceptable encoding found.', context='GZIP') + cherrypy.HTTPError(406, "identity, gzip").set_response() + diff --git a/libs/cherrypy/lib/gctools.py b/libs/cherrypy/lib/gctools.py new file mode 100644 index 0000000..183148b --- /dev/null +++ b/libs/cherrypy/lib/gctools.py @@ -0,0 +1,214 @@ +import gc +import inspect +import os +import sys +import time + +try: + import objgraph +except ImportError: + objgraph = None + +import cherrypy +from cherrypy import _cprequest, _cpwsgi +from cherrypy.process.plugins import SimplePlugin + + +class ReferrerTree(object): + """An object which gathers all referrers of an object to a given depth.""" + + peek_length = 40 + + def __init__(self, ignore=None, maxdepth=2, maxparents=10): + self.ignore = ignore or [] + self.ignore.append(inspect.currentframe().f_back) + self.maxdepth = maxdepth + self.maxparents = maxparents + + def ascend(self, obj, depth=1): + """Return a nested list containing referrers of the given object.""" + depth += 1 + parents = [] + + # Gather all referrers in one step to minimize + # cascading references due to repr() logic. + refs = gc.get_referrers(obj) + self.ignore.append(refs) + if len(refs) > self.maxparents: + return [("[%s referrers]" % len(refs), [])] + + try: + ascendcode = self.ascend.__code__ + except AttributeError: + ascendcode = self.ascend.im_func.func_code + for parent in refs: + if inspect.isframe(parent) and parent.f_code is ascendcode: + continue + if parent in self.ignore: + continue + if depth <= self.maxdepth: + parents.append((parent, self.ascend(parent, depth))) + else: + parents.append((parent, [])) + + return parents + + def peek(self, s): + """Return s, restricted to a sane length.""" + if len(s) > (self.peek_length + 3): + half = self.peek_length // 2 + return s[:half] + '...' + s[-half:] + else: + return s + + def _format(self, obj, descend=True): + """Return a string representation of a single object.""" + if inspect.isframe(obj): + filename, lineno, func, context, index = inspect.getframeinfo(obj) + return "" % func + + if not descend: + return self.peek(repr(obj)) + + if isinstance(obj, dict): + return "{" + ", ".join(["%s: %s" % (self._format(k, descend=False), + self._format(v, descend=False)) + for k, v in obj.items()]) + "}" + elif isinstance(obj, list): + return "[" + ", ".join([self._format(item, descend=False) + for item in obj]) + "]" + elif isinstance(obj, tuple): + return "(" + ", ".join([self._format(item, descend=False) + for item in obj]) + ")" + + r = self.peek(repr(obj)) + if isinstance(obj, (str, int, float)): + return r + return "%s: %s" % (type(obj), r) + + def format(self, tree): + """Return a list of string reprs from a nested list of referrers.""" + output = [] + def ascend(branch, depth=1): + for parent, grandparents in branch: + output.append((" " * depth) + self._format(parent)) + if grandparents: + ascend(grandparents, depth + 1) + ascend(tree) + return output + + +def get_instances(cls): + return [x for x in gc.get_objects() if isinstance(x, cls)] + + +class RequestCounter(SimplePlugin): + + def start(self): + self.count = 0 + + def before_request(self): + self.count += 1 + + def after_request(self): + self.count -=1 +request_counter = RequestCounter(cherrypy.engine) +request_counter.subscribe() + + +def get_context(obj): + if isinstance(obj, _cprequest.Request): + return "path=%s;stage=%s" % (obj.path_info, obj.stage) + elif isinstance(obj, _cprequest.Response): + return "status=%s" % obj.status + elif isinstance(obj, _cpwsgi.AppResponse): + return "PATH_INFO=%s" % obj.environ.get('PATH_INFO', '') + elif hasattr(obj, "tb_lineno"): + return "tb_lineno=%s" % obj.tb_lineno + return "" + + +class GCRoot(object): + """A CherryPy page handler for testing reference leaks.""" + + classes = [(_cprequest.Request, 2, 2, + "Should be 1 in this request thread and 1 in the main thread."), + (_cprequest.Response, 2, 2, + "Should be 1 in this request thread and 1 in the main thread."), + (_cpwsgi.AppResponse, 1, 1, + "Should be 1 in this request thread only."), + ] + + def index(self): + return "Hello, world!" + index.exposed = True + + def stats(self): + output = ["Statistics:"] + + for trial in range(10): + if request_counter.count > 0: + break + time.sleep(0.5) + else: + output.append("\nNot all requests closed properly.") + + # gc_collect isn't perfectly synchronous, because it may + # break reference cycles that then take time to fully + # finalize. Call it thrice and hope for the best. + gc.collect() + gc.collect() + unreachable = gc.collect() + if unreachable: + if objgraph is not None: + final = objgraph.by_type('Nondestructible') + if final: + objgraph.show_backrefs(final, filename='finalizers.png') + + trash = {} + for x in gc.garbage: + trash[type(x)] = trash.get(type(x), 0) + 1 + if trash: + output.insert(0, "\n%s unreachable objects:" % unreachable) + trash = [(v, k) for k, v in trash.items()] + trash.sort() + for pair in trash: + output.append(" " + repr(pair)) + + # Check declared classes to verify uncollected instances. + # These don't have to be part of a cycle; they can be + # any objects that have unanticipated referrers that keep + # them from being collected. + allobjs = {} + for cls, minobj, maxobj, msg in self.classes: + allobjs[cls] = get_instances(cls) + + for cls, minobj, maxobj, msg in self.classes: + objs = allobjs[cls] + lenobj = len(objs) + if lenobj < minobj or lenobj > maxobj: + if minobj == maxobj: + output.append( + "\nExpected %s %r references, got %s." % + (minobj, cls, lenobj)) + else: + output.append( + "\nExpected %s to %s %r references, got %s." % + (minobj, maxobj, cls, lenobj)) + + for obj in objs: + if objgraph is not None: + ig = [id(objs), id(inspect.currentframe())] + fname = "graph_%s_%s.png" % (cls.__name__, id(obj)) + objgraph.show_backrefs( + obj, extra_ignore=ig, max_depth=4, too_many=20, + filename=fname, extra_info=get_context) + output.append("\nReferrers for %s (refcount=%s):" % + (repr(obj), sys.getrefcount(obj))) + t = ReferrerTree(ignore=[objs], maxdepth=3) + tree = t.ascend(obj) + output.extend(t.format(tree)) + + return "\n".join(output) + stats.exposed = True + diff --git a/libs/cherrypy/lib/http.py b/libs/cherrypy/lib/http.py new file mode 100644 index 0000000..4661d69 --- /dev/null +++ b/libs/cherrypy/lib/http.py @@ -0,0 +1,7 @@ +import warnings +warnings.warn('cherrypy.lib.http has been deprecated and will be removed ' + 'in CherryPy 3.3 use cherrypy.lib.httputil instead.', + DeprecationWarning) + +from cherrypy.lib.httputil import * + diff --git a/libs/cherrypy/lib/httpauth.py b/libs/cherrypy/lib/httpauth.py new file mode 100644 index 0000000..ad7c6eb --- /dev/null +++ b/libs/cherrypy/lib/httpauth.py @@ -0,0 +1,354 @@ +""" +This module defines functions to implement HTTP Digest Authentication (:rfc:`2617`). +This has full compliance with 'Digest' and 'Basic' authentication methods. In +'Digest' it supports both MD5 and MD5-sess algorithms. + +Usage: + First use 'doAuth' to request the client authentication for a + certain resource. You should send an httplib.UNAUTHORIZED response to the + client so he knows he has to authenticate itself. + + Then use 'parseAuthorization' to retrieve the 'auth_map' used in + 'checkResponse'. + + To use 'checkResponse' you must have already verified the password associated + with the 'username' key in 'auth_map' dict. Then you use the 'checkResponse' + function to verify if the password matches the one sent by the client. + +SUPPORTED_ALGORITHM - list of supported 'Digest' algorithms +SUPPORTED_QOP - list of supported 'Digest' 'qop'. +""" +__version__ = 1, 0, 1 +__author__ = "Tiago Cogumbreiro " +__credits__ = """ + Peter van Kampen for its recipe which implement most of Digest authentication: + http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/302378 +""" + +__license__ = """ +Copyright (c) 2005, Tiago Cogumbreiro +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of Sylvain Hellegouarch nor the names of his contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +""" + +__all__ = ("digestAuth", "basicAuth", "doAuth", "checkResponse", + "parseAuthorization", "SUPPORTED_ALGORITHM", "md5SessionKey", + "calculateNonce", "SUPPORTED_QOP") + +################################################################################ +import time +from cherrypy._cpcompat import base64_decode, ntob, md5 +from cherrypy._cpcompat import parse_http_list, parse_keqv_list + +MD5 = "MD5" +MD5_SESS = "MD5-sess" +AUTH = "auth" +AUTH_INT = "auth-int" + +SUPPORTED_ALGORITHM = (MD5, MD5_SESS) +SUPPORTED_QOP = (AUTH, AUTH_INT) + +################################################################################ +# doAuth +# +DIGEST_AUTH_ENCODERS = { + MD5: lambda val: md5(ntob(val)).hexdigest(), + MD5_SESS: lambda val: md5(ntob(val)).hexdigest(), +# SHA: lambda val: sha.new(ntob(val)).hexdigest (), +} + +def calculateNonce (realm, algorithm = MD5): + """This is an auxaliary function that calculates 'nonce' value. It is used + to handle sessions.""" + + global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS + assert algorithm in SUPPORTED_ALGORITHM + + try: + encoder = DIGEST_AUTH_ENCODERS[algorithm] + except KeyError: + raise NotImplementedError ("The chosen algorithm (%s) does not have "\ + "an implementation yet" % algorithm) + + return encoder ("%d:%s" % (time.time(), realm)) + +def digestAuth (realm, algorithm = MD5, nonce = None, qop = AUTH): + """Challenges the client for a Digest authentication.""" + global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS, SUPPORTED_QOP + assert algorithm in SUPPORTED_ALGORITHM + assert qop in SUPPORTED_QOP + + if nonce is None: + nonce = calculateNonce (realm, algorithm) + + return 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % ( + realm, nonce, algorithm, qop + ) + +def basicAuth (realm): + """Challengenes the client for a Basic authentication.""" + assert '"' not in realm, "Realms cannot contain the \" (quote) character." + + return 'Basic realm="%s"' % realm + +def doAuth (realm): + """'doAuth' function returns the challenge string b giving priority over + Digest and fallback to Basic authentication when the browser doesn't + support the first one. + + This should be set in the HTTP header under the key 'WWW-Authenticate'.""" + + return digestAuth (realm) + " " + basicAuth (realm) + + +################################################################################ +# Parse authorization parameters +# +def _parseDigestAuthorization (auth_params): + # Convert the auth params to a dict + items = parse_http_list(auth_params) + params = parse_keqv_list(items) + + # Now validate the params + + # Check for required parameters + required = ["username", "realm", "nonce", "uri", "response"] + for k in required: + if k not in params: + return None + + # If qop is sent then cnonce and nc MUST be present + if "qop" in params and not ("cnonce" in params \ + and "nc" in params): + return None + + # If qop is not sent, neither cnonce nor nc can be present + if ("cnonce" in params or "nc" in params) and \ + "qop" not in params: + return None + + return params + + +def _parseBasicAuthorization (auth_params): + username, password = base64_decode(auth_params).split(":", 1) + return {"username": username, "password": password} + +AUTH_SCHEMES = { + "basic": _parseBasicAuthorization, + "digest": _parseDigestAuthorization, +} + +def parseAuthorization (credentials): + """parseAuthorization will convert the value of the 'Authorization' key in + the HTTP header to a map itself. If the parsing fails 'None' is returned. + """ + + global AUTH_SCHEMES + + auth_scheme, auth_params = credentials.split(" ", 1) + auth_scheme = auth_scheme.lower () + + parser = AUTH_SCHEMES[auth_scheme] + params = parser (auth_params) + + if params is None: + return + + assert "auth_scheme" not in params + params["auth_scheme"] = auth_scheme + return params + + +################################################################################ +# Check provided response for a valid password +# +def md5SessionKey (params, password): + """ + If the "algorithm" directive's value is "MD5-sess", then A1 + [the session key] is calculated only once - on the first request by the + client following receipt of a WWW-Authenticate challenge from the server. + + This creates a 'session key' for the authentication of subsequent + requests and responses which is different for each "authentication + session", thus limiting the amount of material hashed with any one + key. + + Because the server need only use the hash of the user + credentials in order to create the A1 value, this construction could + be used in conjunction with a third party authentication service so + that the web server would not need the actual password value. The + specification of such a protocol is beyond the scope of this + specification. +""" + + keys = ("username", "realm", "nonce", "cnonce") + params_copy = {} + for key in keys: + params_copy[key] = params[key] + + params_copy["algorithm"] = MD5_SESS + return _A1 (params_copy, password) + +def _A1(params, password): + algorithm = params.get ("algorithm", MD5) + H = DIGEST_AUTH_ENCODERS[algorithm] + + if algorithm == MD5: + # If the "algorithm" directive's value is "MD5" or is + # unspecified, then A1 is: + # A1 = unq(username-value) ":" unq(realm-value) ":" passwd + return "%s:%s:%s" % (params["username"], params["realm"], password) + + elif algorithm == MD5_SESS: + + # This is A1 if qop is set + # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd ) + # ":" unq(nonce-value) ":" unq(cnonce-value) + h_a1 = H ("%s:%s:%s" % (params["username"], params["realm"], password)) + return "%s:%s:%s" % (h_a1, params["nonce"], params["cnonce"]) + + +def _A2(params, method, kwargs): + # If the "qop" directive's value is "auth" or is unspecified, then A2 is: + # A2 = Method ":" digest-uri-value + + qop = params.get ("qop", "auth") + if qop == "auth": + return method + ":" + params["uri"] + elif qop == "auth-int": + # If the "qop" value is "auth-int", then A2 is: + # A2 = Method ":" digest-uri-value ":" H(entity-body) + entity_body = kwargs.get ("entity_body", "") + H = kwargs["H"] + + return "%s:%s:%s" % ( + method, + params["uri"], + H(entity_body) + ) + + else: + raise NotImplementedError ("The 'qop' method is unknown: %s" % qop) + +def _computeDigestResponse(auth_map, password, method = "GET", A1 = None,**kwargs): + """ + Generates a response respecting the algorithm defined in RFC 2617 + """ + params = auth_map + + algorithm = params.get ("algorithm", MD5) + + H = DIGEST_AUTH_ENCODERS[algorithm] + KD = lambda secret, data: H(secret + ":" + data) + + qop = params.get ("qop", None) + + H_A2 = H(_A2(params, method, kwargs)) + + if algorithm == MD5_SESS and A1 is not None: + H_A1 = H(A1) + else: + H_A1 = H(_A1(params, password)) + + if qop in ("auth", "auth-int"): + # If the "qop" value is "auth" or "auth-int": + # request-digest = <"> < KD ( H(A1), unq(nonce-value) + # ":" nc-value + # ":" unq(cnonce-value) + # ":" unq(qop-value) + # ":" H(A2) + # ) <"> + request = "%s:%s:%s:%s:%s" % ( + params["nonce"], + params["nc"], + params["cnonce"], + params["qop"], + H_A2, + ) + elif qop is None: + # If the "qop" directive is not present (this construction is + # for compatibility with RFC 2069): + # request-digest = + # <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <"> + request = "%s:%s" % (params["nonce"], H_A2) + + return KD(H_A1, request) + +def _checkDigestResponse(auth_map, password, method = "GET", A1 = None, **kwargs): + """This function is used to verify the response given by the client when + he tries to authenticate. + Optional arguments: + entity_body - when 'qop' is set to 'auth-int' you MUST provide the + raw data you are going to send to the client (usually the + HTML page. + request_uri - the uri from the request line compared with the 'uri' + directive of the authorization map. They must represent + the same resource (unused at this time). + """ + + if auth_map['realm'] != kwargs.get('realm', None): + return False + + response = _computeDigestResponse(auth_map, password, method, A1,**kwargs) + + return response == auth_map["response"] + +def _checkBasicResponse (auth_map, password, method='GET', encrypt=None, **kwargs): + # Note that the Basic response doesn't provide the realm value so we cannot + # test it + try: + return encrypt(auth_map["password"], auth_map["username"]) == password + except TypeError: + return encrypt(auth_map["password"]) == password + +AUTH_RESPONSES = { + "basic": _checkBasicResponse, + "digest": _checkDigestResponse, +} + +def checkResponse (auth_map, password, method = "GET", encrypt=None, **kwargs): + """'checkResponse' compares the auth_map with the password and optionally + other arguments that each implementation might need. + + If the response is of type 'Basic' then the function has the following + signature:: + + checkBasicResponse (auth_map, password) -> bool + + If the response is of type 'Digest' then the function has the following + signature:: + + checkDigestResponse (auth_map, password, method = 'GET', A1 = None) -> bool + + The 'A1' argument is only used in MD5_SESS algorithm based responses. + Check md5SessionKey() for more info. + """ + checker = AUTH_RESPONSES[auth_map["auth_scheme"]] + return checker (auth_map, password, method=method, encrypt=encrypt, **kwargs) + + + + diff --git a/libs/cherrypy/lib/httputil.py b/libs/cherrypy/lib/httputil.py new file mode 100644 index 0000000..5f77d54 --- /dev/null +++ b/libs/cherrypy/lib/httputil.py @@ -0,0 +1,506 @@ +"""HTTP library functions. + +This module contains functions for building an HTTP application +framework: any one, not just one whose name starts with "Ch". ;) If you +reference any modules from some popular framework inside *this* module, +FuManChu will personally hang you up by your thumbs and submit you +to a public caning. +""" + +from binascii import b2a_base64 +from cherrypy._cpcompat import BaseHTTPRequestHandler, HTTPDate, ntob, ntou, reversed, sorted +from cherrypy._cpcompat import basestring, bytestr, iteritems, nativestr, unicodestr, unquote_qs +response_codes = BaseHTTPRequestHandler.responses.copy() + +# From http://www.cherrypy.org/ticket/361 +response_codes[500] = ('Internal Server Error', + 'The server encountered an unexpected condition ' + 'which prevented it from fulfilling the request.') +response_codes[503] = ('Service Unavailable', + 'The server is currently unable to handle the ' + 'request due to a temporary overloading or ' + 'maintenance of the server.') + +import re +import urllib + + + +def urljoin(*atoms): + """Return the given path \*atoms, joined into a single URL. + + This will correctly join a SCRIPT_NAME and PATH_INFO into the + original URL, even if either atom is blank. + """ + url = "/".join([x for x in atoms if x]) + while "//" in url: + url = url.replace("//", "/") + # Special-case the final url of "", and return "/" instead. + return url or "/" + +def urljoin_bytes(*atoms): + """Return the given path *atoms, joined into a single URL. + + This will correctly join a SCRIPT_NAME and PATH_INFO into the + original URL, even if either atom is blank. + """ + url = ntob("/").join([x for x in atoms if x]) + while ntob("//") in url: + url = url.replace(ntob("//"), ntob("/")) + # Special-case the final url of "", and return "/" instead. + return url or ntob("/") + +def protocol_from_http(protocol_str): + """Return a protocol tuple from the given 'HTTP/x.y' string.""" + return int(protocol_str[5]), int(protocol_str[7]) + +def get_ranges(headervalue, content_length): + """Return a list of (start, stop) indices from a Range header, or None. + + Each (start, stop) tuple will be composed of two ints, which are suitable + for use in a slicing operation. That is, the header "Range: bytes=3-6", + if applied against a Python string, is requesting resource[3:7]. This + function will return the list [(3, 7)]. + + If this function returns an empty list, you should return HTTP 416. + """ + + if not headervalue: + return None + + result = [] + bytesunit, byteranges = headervalue.split("=", 1) + for brange in byteranges.split(","): + start, stop = [x.strip() for x in brange.split("-", 1)] + if start: + if not stop: + stop = content_length - 1 + start, stop = int(start), int(stop) + if start >= content_length: + # From rfc 2616 sec 14.16: + # "If the server receives a request (other than one + # including an If-Range request-header field) with an + # unsatisfiable Range request-header field (that is, + # all of whose byte-range-spec values have a first-byte-pos + # value greater than the current length of the selected + # resource), it SHOULD return a response code of 416 + # (Requested range not satisfiable)." + continue + if stop < start: + # From rfc 2616 sec 14.16: + # "If the server ignores a byte-range-spec because it + # is syntactically invalid, the server SHOULD treat + # the request as if the invalid Range header field + # did not exist. (Normally, this means return a 200 + # response containing the full entity)." + return None + result.append((start, stop + 1)) + else: + if not stop: + # See rfc quote above. + return None + # Negative subscript (last N bytes) + result.append((content_length - int(stop), content_length)) + + return result + + +class HeaderElement(object): + """An element (with parameters) from an HTTP header's element list.""" + + def __init__(self, value, params=None): + self.value = value + if params is None: + params = {} + self.params = params + + def __cmp__(self, other): + return cmp(self.value, other.value) + + def __lt__(self, other): + return self.value < other.value + + def __str__(self): + p = [";%s=%s" % (k, v) for k, v in iteritems(self.params)] + return "%s%s" % (self.value, "".join(p)) + + def __bytes__(self): + return ntob(self.__str__()) + + def __unicode__(self): + return ntou(self.__str__()) + + def parse(elementstr): + """Transform 'token;key=val' to ('token', {'key': 'val'}).""" + # Split the element into a value and parameters. The 'value' may + # be of the form, "token=token", but we don't split that here. + atoms = [x.strip() for x in elementstr.split(";") if x.strip()] + if not atoms: + initial_value = '' + else: + initial_value = atoms.pop(0).strip() + params = {} + for atom in atoms: + atom = [x.strip() for x in atom.split("=", 1) if x.strip()] + key = atom.pop(0) + if atom: + val = atom[0] + else: + val = "" + params[key] = val + return initial_value, params + parse = staticmethod(parse) + + def from_str(cls, elementstr): + """Construct an instance from a string of the form 'token;key=val'.""" + ival, params = cls.parse(elementstr) + return cls(ival, params) + from_str = classmethod(from_str) + + +q_separator = re.compile(r'; *q *=') + +class AcceptElement(HeaderElement): + """An element (with parameters) from an Accept* header's element list. + + AcceptElement objects are comparable; the more-preferred object will be + "less than" the less-preferred object. They are also therefore sortable; + if you sort a list of AcceptElement objects, they will be listed in + priority order; the most preferred value will be first. Yes, it should + have been the other way around, but it's too late to fix now. + """ + + def from_str(cls, elementstr): + qvalue = None + # The first "q" parameter (if any) separates the initial + # media-range parameter(s) (if any) from the accept-params. + atoms = q_separator.split(elementstr, 1) + media_range = atoms.pop(0).strip() + if atoms: + # The qvalue for an Accept header can have extensions. The other + # headers cannot, but it's easier to parse them as if they did. + qvalue = HeaderElement.from_str(atoms[0].strip()) + + media_type, params = cls.parse(media_range) + if qvalue is not None: + params["q"] = qvalue + return cls(media_type, params) + from_str = classmethod(from_str) + + def qvalue(self): + val = self.params.get("q", "1") + if isinstance(val, HeaderElement): + val = val.value + return float(val) + qvalue = property(qvalue, doc="The qvalue, or priority, of this value.") + + def __cmp__(self, other): + diff = cmp(self.qvalue, other.qvalue) + if diff == 0: + diff = cmp(str(self), str(other)) + return diff + + def __lt__(self, other): + if self.qvalue == other.qvalue: + return str(self) < str(other) + else: + return self.qvalue < other.qvalue + + +def header_elements(fieldname, fieldvalue): + """Return a sorted HeaderElement list from a comma-separated header string.""" + if not fieldvalue: + return [] + + result = [] + for element in fieldvalue.split(","): + if fieldname.startswith("Accept") or fieldname == 'TE': + hv = AcceptElement.from_str(element) + else: + hv = HeaderElement.from_str(element) + result.append(hv) + + return list(reversed(sorted(result))) + +def decode_TEXT(value): + r"""Decode :rfc:`2047` TEXT (e.g. "=?utf-8?q?f=C3=BCr?=" -> "f\xfcr").""" + try: + # Python 3 + from email.header import decode_header + except ImportError: + from email.Header import decode_header + atoms = decode_header(value) + decodedvalue = "" + for atom, charset in atoms: + if charset is not None: + atom = atom.decode(charset) + decodedvalue += atom + return decodedvalue + +def valid_status(status): + """Return legal HTTP status Code, Reason-phrase and Message. + + The status arg must be an int, or a str that begins with an int. + + If status is an int, or a str and no reason-phrase is supplied, + a default reason-phrase will be provided. + """ + + if not status: + status = 200 + + status = str(status) + parts = status.split(" ", 1) + if len(parts) == 1: + # No reason supplied. + code, = parts + reason = None + else: + code, reason = parts + reason = reason.strip() + + try: + code = int(code) + except ValueError: + raise ValueError("Illegal response status from server " + "(%s is non-numeric)." % repr(code)) + + if code < 100 or code > 599: + raise ValueError("Illegal response status from server " + "(%s is out of range)." % repr(code)) + + if code not in response_codes: + # code is unknown but not illegal + default_reason, message = "", "" + else: + default_reason, message = response_codes[code] + + if reason is None: + reason = default_reason + + return code, reason, message + + +# NOTE: the parse_qs functions that follow are modified version of those +# in the python3.0 source - we need to pass through an encoding to the unquote +# method, but the default parse_qs function doesn't allow us to. These do. + +def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'): + """Parse a query given as a string argument. + + Arguments: + + qs: URL-encoded query string to be parsed + + keep_blank_values: flag indicating whether blank values in + URL encoded queries should be treated as blank strings. A + true value indicates that blanks should be retained as blank + strings. The default false value indicates that blank values + are to be ignored and treated as if they were not included. + + strict_parsing: flag indicating what to do with parsing errors. If + false (the default), errors are silently ignored. If true, + errors raise a ValueError exception. + + Returns a dict, as G-d intended. + """ + pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')] + d = {} + for name_value in pairs: + if not name_value and not strict_parsing: + continue + nv = name_value.split('=', 1) + if len(nv) != 2: + if strict_parsing: + raise ValueError("bad query field: %r" % (name_value,)) + # Handle case of a control-name with no equal sign + if keep_blank_values: + nv.append('') + else: + continue + if len(nv[1]) or keep_blank_values: + name = unquote_qs(nv[0], encoding) + value = unquote_qs(nv[1], encoding) + if name in d: + if not isinstance(d[name], list): + d[name] = [d[name]] + d[name].append(value) + else: + d[name] = value + return d + + +image_map_pattern = re.compile(r"[0-9]+,[0-9]+") + +def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'): + """Build a params dictionary from a query_string. + + Duplicate key/value pairs in the provided query_string will be + returned as {'key': [val1, val2, ...]}. Single key/values will + be returned as strings: {'key': 'value'}. + """ + if image_map_pattern.match(query_string): + # Server-side image map. Map the coords to 'x' and 'y' + # (like CGI::Request does). + pm = query_string.split(",") + pm = {'x': int(pm[0]), 'y': int(pm[1])} + else: + pm = _parse_qs(query_string, keep_blank_values, encoding=encoding) + return pm + + +class CaseInsensitiveDict(dict): + """A case-insensitive dict subclass. + + Each key is changed on entry to str(key).title(). + """ + + def __getitem__(self, key): + return dict.__getitem__(self, str(key).title()) + + def __setitem__(self, key, value): + dict.__setitem__(self, str(key).title(), value) + + def __delitem__(self, key): + dict.__delitem__(self, str(key).title()) + + def __contains__(self, key): + return dict.__contains__(self, str(key).title()) + + def get(self, key, default=None): + return dict.get(self, str(key).title(), default) + + if hasattr({}, 'has_key'): + def has_key(self, key): + return dict.has_key(self, str(key).title()) + + def update(self, E): + for k in E.keys(): + self[str(k).title()] = E[k] + + def fromkeys(cls, seq, value=None): + newdict = cls() + for k in seq: + newdict[str(k).title()] = value + return newdict + fromkeys = classmethod(fromkeys) + + def setdefault(self, key, x=None): + key = str(key).title() + try: + return self[key] + except KeyError: + self[key] = x + return x + + def pop(self, key, default): + return dict.pop(self, str(key).title(), default) + + +# TEXT = +# +# A CRLF is allowed in the definition of TEXT only as part of a header +# field continuation. It is expected that the folding LWS will be +# replaced with a single SP before interpretation of the TEXT value." +if nativestr == bytestr: + header_translate_table = ''.join([chr(i) for i in xrange(256)]) + header_translate_deletechars = ''.join([chr(i) for i in xrange(32)]) + chr(127) +else: + header_translate_table = None + header_translate_deletechars = bytes(range(32)) + bytes([127]) + + +class HeaderMap(CaseInsensitiveDict): + """A dict subclass for HTTP request and response headers. + + Each key is changed on entry to str(key).title(). This allows headers + to be case-insensitive and avoid duplicates. + + Values are header values (decoded according to :rfc:`2047` if necessary). + """ + + protocol=(1, 1) + encodings = ["ISO-8859-1"] + + # Someday, when http-bis is done, this will probably get dropped + # since few servers, clients, or intermediaries do it. But until then, + # we're going to obey the spec as is. + # "Words of *TEXT MAY contain characters from character sets other than + # ISO-8859-1 only when encoded according to the rules of RFC 2047." + use_rfc_2047 = True + + def elements(self, key): + """Return a sorted list of HeaderElements for the given header.""" + key = str(key).title() + value = self.get(key) + return header_elements(key, value) + + def values(self, key): + """Return a sorted list of HeaderElement.value for the given header.""" + return [e.value for e in self.elements(key)] + + def output(self): + """Transform self into a list of (name, value) tuples.""" + header_list = [] + for k, v in self.items(): + if isinstance(k, unicodestr): + k = self.encode(k) + + if not isinstance(v, basestring): + v = str(v) + + if isinstance(v, unicodestr): + v = self.encode(v) + + # See header_translate_* constants above. + # Replace only if you really know what you're doing. + k = k.translate(header_translate_table, header_translate_deletechars) + v = v.translate(header_translate_table, header_translate_deletechars) + + header_list.append((k, v)) + return header_list + + def encode(self, v): + """Return the given header name or value, encoded for HTTP output.""" + for enc in self.encodings: + try: + return v.encode(enc) + except UnicodeEncodeError: + continue + + if self.protocol == (1, 1) and self.use_rfc_2047: + # Encode RFC-2047 TEXT + # (e.g. u"\u8200" -> "=?utf-8?b?6IiA?="). + # We do our own here instead of using the email module + # because we never want to fold lines--folding has + # been deprecated by the HTTP working group. + v = b2a_base64(v.encode('utf-8')) + return (ntob('=?utf-8?b?') + v.strip(ntob('\n')) + ntob('?=')) + + raise ValueError("Could not encode header part %r using " + "any of the encodings %r." % + (v, self.encodings)) + + +class Host(object): + """An internet address. + + name + Should be the client's host name. If not available (because no DNS + lookup is performed), the IP address should be used instead. + + """ + + ip = "0.0.0.0" + port = 80 + name = "unknown.tld" + + def __init__(self, ip, port, name=None): + self.ip = ip + self.port = port + if name is None: + name = ip + self.name = name + + def __repr__(self): + return "httputil.Host(%r, %r, %r)" % (self.ip, self.port, self.name) diff --git a/libs/cherrypy/lib/jsontools.py b/libs/cherrypy/lib/jsontools.py new file mode 100644 index 0000000..2092579 --- /dev/null +++ b/libs/cherrypy/lib/jsontools.py @@ -0,0 +1,87 @@ +import sys +import cherrypy +from cherrypy._cpcompat import basestring, ntou, json, json_encode, json_decode + +def json_processor(entity): + """Read application/json data into request.json.""" + if not entity.headers.get(ntou("Content-Length"), ntou("")): + raise cherrypy.HTTPError(411) + + body = entity.fp.read() + try: + cherrypy.serving.request.json = json_decode(body.decode('utf-8')) + except ValueError: + raise cherrypy.HTTPError(400, 'Invalid JSON document') + +def json_in(content_type=[ntou('application/json'), ntou('text/javascript')], + force=True, debug=False, processor = json_processor): + """Add a processor to parse JSON request entities: + The default processor places the parsed data into request.json. + + Incoming request entities which match the given content_type(s) will + be deserialized from JSON to the Python equivalent, and the result + stored at cherrypy.request.json. The 'content_type' argument may + be a Content-Type string or a list of allowable Content-Type strings. + + If the 'force' argument is True (the default), then entities of other + content types will not be allowed; "415 Unsupported Media Type" is + raised instead. + + Supply your own processor to use a custom decoder, or to handle the parsed + data differently. The processor can be configured via + tools.json_in.processor or via the decorator method. + + Note that the deserializer requires the client send a Content-Length + request header, or it will raise "411 Length Required". If for any + other reason the request entity cannot be deserialized from JSON, + it will raise "400 Bad Request: Invalid JSON document". + + You must be using Python 2.6 or greater, or have the 'simplejson' + package importable; otherwise, ValueError is raised during processing. + """ + request = cherrypy.serving.request + if isinstance(content_type, basestring): + content_type = [content_type] + + if force: + if debug: + cherrypy.log('Removing body processors %s' % + repr(request.body.processors.keys()), 'TOOLS.JSON_IN') + request.body.processors.clear() + request.body.default_proc = cherrypy.HTTPError( + 415, 'Expected an entity of content type %s' % + ', '.join(content_type)) + + for ct in content_type: + if debug: + cherrypy.log('Adding body processor for %s' % ct, 'TOOLS.JSON_IN') + request.body.processors[ct] = processor + +def json_handler(*args, **kwargs): + value = cherrypy.serving.request._json_inner_handler(*args, **kwargs) + return json_encode(value) + +def json_out(content_type='application/json', debug=False, handler=json_handler): + """Wrap request.handler to serialize its output to JSON. Sets Content-Type. + + If the given content_type is None, the Content-Type response header + is not set. + + Provide your own handler to use a custom encoder. For example + cherrypy.config['tools.json_out.handler'] = , or + @json_out(handler=function). + + You must be using Python 2.6 or greater, or have the 'simplejson' + package importable; otherwise, ValueError is raised during processing. + """ + request = cherrypy.serving.request + if debug: + cherrypy.log('Replacing %s with JSON handler' % request.handler, + 'TOOLS.JSON_OUT') + request._json_inner_handler = request.handler + request.handler = handler + if content_type is not None: + if debug: + cherrypy.log('Setting Content-Type to %s' % content_type, 'TOOLS.JSON_OUT') + cherrypy.serving.response.headers['Content-Type'] = content_type + diff --git a/libs/cherrypy/lib/profiler.py b/libs/cherrypy/lib/profiler.py new file mode 100644 index 0000000..785d58a --- /dev/null +++ b/libs/cherrypy/lib/profiler.py @@ -0,0 +1,208 @@ +"""Profiler tools for CherryPy. + +CherryPy users +============== + +You can profile any of your pages as follows:: + + from cherrypy.lib import profiler + + class Root: + p = profile.Profiler("/path/to/profile/dir") + + def index(self): + self.p.run(self._index) + index.exposed = True + + def _index(self): + return "Hello, world!" + + cherrypy.tree.mount(Root()) + +You can also turn on profiling for all requests +using the ``make_app`` function as WSGI middleware. + +CherryPy developers +=================== + +This module can be used whenever you make changes to CherryPy, +to get a quick sanity-check on overall CP performance. Use the +``--profile`` flag when running the test suite. Then, use the ``serve()`` +function to browse the results in a web browser. If you run this +module from the command line, it will call ``serve()`` for you. + +""" + + +def new_func_strip_path(func_name): + """Make profiler output more readable by adding ``__init__`` modules' parents""" + filename, line, name = func_name + if filename.endswith("__init__.py"): + return os.path.basename(filename[:-12]) + filename[-12:], line, name + return os.path.basename(filename), line, name + +try: + import profile + import pstats + pstats.func_strip_path = new_func_strip_path +except ImportError: + profile = None + pstats = None + +import os, os.path +import sys +import warnings + +from cherrypy._cpcompat import BytesIO + +_count = 0 + +class Profiler(object): + + def __init__(self, path=None): + if not path: + path = os.path.join(os.path.dirname(__file__), "profile") + self.path = path + if not os.path.exists(path): + os.makedirs(path) + + def run(self, func, *args, **params): + """Dump profile data into self.path.""" + global _count + c = _count = _count + 1 + path = os.path.join(self.path, "cp_%04d.prof" % c) + prof = profile.Profile() + result = prof.runcall(func, *args, **params) + prof.dump_stats(path) + return result + + def statfiles(self): + """:rtype: list of available profiles. + """ + return [f for f in os.listdir(self.path) + if f.startswith("cp_") and f.endswith(".prof")] + + def stats(self, filename, sortby='cumulative'): + """:rtype stats(index): output of print_stats() for the given profile. + """ + sio = BytesIO() + if sys.version_info >= (2, 5): + s = pstats.Stats(os.path.join(self.path, filename), stream=sio) + s.strip_dirs() + s.sort_stats(sortby) + s.print_stats() + else: + # pstats.Stats before Python 2.5 didn't take a 'stream' arg, + # but just printed to stdout. So re-route stdout. + s = pstats.Stats(os.path.join(self.path, filename)) + s.strip_dirs() + s.sort_stats(sortby) + oldout = sys.stdout + try: + sys.stdout = sio + s.print_stats() + finally: + sys.stdout = oldout + response = sio.getvalue() + sio.close() + return response + + def index(self): + return """ + CherryPy profile data + + + + + + """ + index.exposed = True + + def menu(self): + yield "

Profiling runs

" + yield "

Click on one of the runs below to see profiling data.

" + runs = self.statfiles() + runs.sort() + for i in runs: + yield "%s
" % (i, i) + menu.exposed = True + + def report(self, filename): + import cherrypy + cherrypy.response.headers['Content-Type'] = 'text/plain' + return self.stats(filename) + report.exposed = True + + +class ProfileAggregator(Profiler): + + def __init__(self, path=None): + Profiler.__init__(self, path) + global _count + self.count = _count = _count + 1 + self.profiler = profile.Profile() + + def run(self, func, *args): + path = os.path.join(self.path, "cp_%04d.prof" % self.count) + result = self.profiler.runcall(func, *args) + self.profiler.dump_stats(path) + return result + + +class make_app: + def __init__(self, nextapp, path=None, aggregate=False): + """Make a WSGI middleware app which wraps 'nextapp' with profiling. + + nextapp + the WSGI application to wrap, usually an instance of + cherrypy.Application. + + path + where to dump the profiling output. + + aggregate + if True, profile data for all HTTP requests will go in + a single file. If False (the default), each HTTP request will + dump its profile data into a separate file. + + """ + if profile is None or pstats is None: + msg = ("Your installation of Python does not have a profile module. " + "If you're on Debian, try `sudo apt-get install python-profiler`. " + "See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.") + warnings.warn(msg) + + self.nextapp = nextapp + self.aggregate = aggregate + if aggregate: + self.profiler = ProfileAggregator(path) + else: + self.profiler = Profiler(path) + + def __call__(self, environ, start_response): + def gather(): + result = [] + for line in self.nextapp(environ, start_response): + result.append(line) + return result + return self.profiler.run(gather) + + +def serve(path=None, port=8080): + if profile is None or pstats is None: + msg = ("Your installation of Python does not have a profile module. " + "If you're on Debian, try `sudo apt-get install python-profiler`. " + "See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.") + warnings.warn(msg) + + import cherrypy + cherrypy.config.update({'server.socket_port': int(port), + 'server.thread_pool': 10, + 'environment': "production", + }) + cherrypy.quickstart(Profiler(path)) + + +if __name__ == "__main__": + serve(*tuple(sys.argv[1:])) + diff --git a/libs/cherrypy/lib/reprconf.py b/libs/cherrypy/lib/reprconf.py new file mode 100644 index 0000000..ba8ff51 --- /dev/null +++ b/libs/cherrypy/lib/reprconf.py @@ -0,0 +1,485 @@ +"""Generic configuration system using unrepr. + +Configuration data may be supplied as a Python dictionary, as a filename, +or as an open file object. When you supply a filename or file, Python's +builtin ConfigParser is used (with some extensions). + +Namespaces +---------- + +Configuration keys are separated into namespaces by the first "." in the key. + +The only key that cannot exist in a namespace is the "environment" entry. +This special entry 'imports' other config entries from a template stored in +the Config.environments dict. + +You can define your own namespaces to be called when new config is merged +by adding a named handler to Config.namespaces. The name can be any string, +and the handler must be either a callable or a context manager. +""" + +try: + # Python 3.0+ + from configparser import ConfigParser +except ImportError: + from ConfigParser import ConfigParser + +try: + set +except NameError: + from sets import Set as set + +try: + basestring +except NameError: + basestring = str + +try: + # Python 3 + import builtins +except ImportError: + # Python 2 + import __builtin__ as builtins + +import operator as _operator +import sys + +def as_dict(config): + """Return a dict from 'config' whether it is a dict, file, or filename.""" + if isinstance(config, basestring): + config = Parser().dict_from_file(config) + elif hasattr(config, 'read'): + config = Parser().dict_from_file(config) + return config + + +class NamespaceSet(dict): + """A dict of config namespace names and handlers. + + Each config entry should begin with a namespace name; the corresponding + namespace handler will be called once for each config entry in that + namespace, and will be passed two arguments: the config key (with the + namespace removed) and the config value. + + Namespace handlers may be any Python callable; they may also be + Python 2.5-style 'context managers', in which case their __enter__ + method should return a callable to be used as the handler. + See cherrypy.tools (the Toolbox class) for an example. + """ + + def __call__(self, config): + """Iterate through config and pass it to each namespace handler. + + config + A flat dict, where keys use dots to separate + namespaces, and values are arbitrary. + + The first name in each config key is used to look up the corresponding + namespace handler. For example, a config entry of {'tools.gzip.on': v} + will call the 'tools' namespace handler with the args: ('gzip.on', v) + """ + # Separate the given config into namespaces + ns_confs = {} + for k in config: + if "." in k: + ns, name = k.split(".", 1) + bucket = ns_confs.setdefault(ns, {}) + bucket[name] = config[k] + + # I chose __enter__ and __exit__ so someday this could be + # rewritten using Python 2.5's 'with' statement: + # for ns, handler in self.iteritems(): + # with handler as callable: + # for k, v in ns_confs.get(ns, {}).iteritems(): + # callable(k, v) + for ns, handler in self.items(): + exit = getattr(handler, "__exit__", None) + if exit: + callable = handler.__enter__() + no_exc = True + try: + try: + for k, v in ns_confs.get(ns, {}).items(): + callable(k, v) + except: + # The exceptional case is handled here + no_exc = False + if exit is None: + raise + if not exit(*sys.exc_info()): + raise + # The exception is swallowed if exit() returns true + finally: + # The normal and non-local-goto cases are handled here + if no_exc and exit: + exit(None, None, None) + else: + for k, v in ns_confs.get(ns, {}).items(): + handler(k, v) + + def __repr__(self): + return "%s.%s(%s)" % (self.__module__, self.__class__.__name__, + dict.__repr__(self)) + + def __copy__(self): + newobj = self.__class__() + newobj.update(self) + return newobj + copy = __copy__ + + +class Config(dict): + """A dict-like set of configuration data, with defaults and namespaces. + + May take a file, filename, or dict. + """ + + defaults = {} + environments = {} + namespaces = NamespaceSet() + + def __init__(self, file=None, **kwargs): + self.reset() + if file is not None: + self.update(file) + if kwargs: + self.update(kwargs) + + def reset(self): + """Reset self to default values.""" + self.clear() + dict.update(self, self.defaults) + + def update(self, config): + """Update self from a dict, file or filename.""" + if isinstance(config, basestring): + # Filename + config = Parser().dict_from_file(config) + elif hasattr(config, 'read'): + # Open file object + config = Parser().dict_from_file(config) + else: + config = config.copy() + self._apply(config) + + def _apply(self, config): + """Update self from a dict.""" + which_env = config.get('environment') + if which_env: + env = self.environments[which_env] + for k in env: + if k not in config: + config[k] = env[k] + + dict.update(self, config) + self.namespaces(config) + + def __setitem__(self, k, v): + dict.__setitem__(self, k, v) + self.namespaces({k: v}) + + +class Parser(ConfigParser): + """Sub-class of ConfigParser that keeps the case of options and that + raises an exception if the file cannot be read. + """ + + def optionxform(self, optionstr): + return optionstr + + def read(self, filenames): + if isinstance(filenames, basestring): + filenames = [filenames] + for filename in filenames: + # try: + # fp = open(filename) + # except IOError: + # continue + fp = open(filename) + try: + self._read(fp, filename) + finally: + fp.close() + + def as_dict(self, raw=False, vars=None): + """Convert an INI file to a dictionary""" + # Load INI file into a dict + result = {} + for section in self.sections(): + if section not in result: + result[section] = {} + for option in self.options(section): + value = self.get(section, option, raw=raw, vars=vars) + try: + value = unrepr(value) + except Exception: + x = sys.exc_info()[1] + msg = ("Config error in section: %r, option: %r, " + "value: %r. Config values must be valid Python." % + (section, option, value)) + raise ValueError(msg, x.__class__.__name__, x.args) + result[section][option] = value + return result + + def dict_from_file(self, file): + if hasattr(file, 'read'): + self.readfp(file) + else: + self.read(file) + return self.as_dict() + + +# public domain "unrepr" implementation, found on the web and then improved. + + +class _Builder2: + + def build(self, o): + m = getattr(self, 'build_' + o.__class__.__name__, None) + if m is None: + raise TypeError("unrepr does not recognize %s" % + repr(o.__class__.__name__)) + return m(o) + + def astnode(self, s): + """Return a Python2 ast Node compiled from a string.""" + try: + import compiler + except ImportError: + # Fallback to eval when compiler package is not available, + # e.g. IronPython 1.0. + return eval(s) + + p = compiler.parse("__tempvalue__ = " + s) + return p.getChildren()[1].getChildren()[0].getChildren()[1] + + def build_Subscript(self, o): + expr, flags, subs = o.getChildren() + expr = self.build(expr) + subs = self.build(subs) + return expr[subs] + + def build_CallFunc(self, o): + children = map(self.build, o.getChildren()) + callee = children.pop(0) + kwargs = children.pop() or {} + starargs = children.pop() or () + args = tuple(children) + tuple(starargs) + return callee(*args, **kwargs) + + def build_List(self, o): + return map(self.build, o.getChildren()) + + def build_Const(self, o): + return o.value + + def build_Dict(self, o): + d = {} + i = iter(map(self.build, o.getChildren())) + for el in i: + d[el] = i.next() + return d + + def build_Tuple(self, o): + return tuple(self.build_List(o)) + + def build_Name(self, o): + name = o.name + if name == 'None': + return None + if name == 'True': + return True + if name == 'False': + return False + + # See if the Name is a package or module. If it is, import it. + try: + return modules(name) + except ImportError: + pass + + # See if the Name is in builtins. + try: + return getattr(builtins, name) + except AttributeError: + pass + + raise TypeError("unrepr could not resolve the name %s" % repr(name)) + + def build_Add(self, o): + left, right = map(self.build, o.getChildren()) + return left + right + + def build_Mul(self, o): + left, right = map(self.build, o.getChildren()) + return left * right + + def build_Getattr(self, o): + parent = self.build(o.expr) + return getattr(parent, o.attrname) + + def build_NoneType(self, o): + return None + + def build_UnarySub(self, o): + return -self.build(o.getChildren()[0]) + + def build_UnaryAdd(self, o): + return self.build(o.getChildren()[0]) + + +class _Builder3: + + def build(self, o): + m = getattr(self, 'build_' + o.__class__.__name__, None) + if m is None: + raise TypeError("unrepr does not recognize %s" % + repr(o.__class__.__name__)) + return m(o) + + def astnode(self, s): + """Return a Python3 ast Node compiled from a string.""" + try: + import ast + except ImportError: + # Fallback to eval when ast package is not available, + # e.g. IronPython 1.0. + return eval(s) + + p = ast.parse("__tempvalue__ = " + s) + return p.body[0].value + + def build_Subscript(self, o): + return self.build(o.value)[self.build(o.slice)] + + def build_Index(self, o): + return self.build(o.value) + + def build_Call(self, o): + callee = self.build(o.func) + + if o.args is None: + args = () + else: + args = tuple([self.build(a) for a in o.args]) + + if o.starargs is None: + starargs = () + else: + starargs = self.build(o.starargs) + + if o.kwargs is None: + kwargs = {} + else: + kwargs = self.build(o.kwargs) + + return callee(*(args + starargs), **kwargs) + + def build_List(self, o): + return list(map(self.build, o.elts)) + + def build_Str(self, o): + return o.s + + def build_Num(self, o): + return o.n + + def build_Dict(self, o): + return dict([(self.build(k), self.build(v)) + for k, v in zip(o.keys, o.values)]) + + def build_Tuple(self, o): + return tuple(self.build_List(o)) + + def build_Name(self, o): + name = o.id + if name == 'None': + return None + if name == 'True': + return True + if name == 'False': + return False + + # See if the Name is a package or module. If it is, import it. + try: + return modules(name) + except ImportError: + pass + + # See if the Name is in builtins. + try: + import builtins + return getattr(builtins, name) + except AttributeError: + pass + + raise TypeError("unrepr could not resolve the name %s" % repr(name)) + + def build_UnaryOp(self, o): + op, operand = map(self.build, [o.op, o.operand]) + return op(operand) + + def build_BinOp(self, o): + left, op, right = map(self.build, [o.left, o.op, o.right]) + return op(left, right) + + def build_Add(self, o): + return _operator.add + + def build_Mult(self, o): + return _operator.mul + + def build_USub(self, o): + return _operator.neg + + def build_Attribute(self, o): + parent = self.build(o.value) + return getattr(parent, o.attr) + + def build_NoneType(self, o): + return None + + +def unrepr(s): + """Return a Python object compiled from a string.""" + if not s: + return s + if sys.version_info < (3, 0): + b = _Builder2() + else: + b = _Builder3() + obj = b.astnode(s) + return b.build(obj) + + +def modules(modulePath): + """Load a module and retrieve a reference to that module.""" + try: + mod = sys.modules[modulePath] + if mod is None: + raise KeyError() + except KeyError: + # The last [''] is important. + mod = __import__(modulePath, globals(), locals(), ['']) + return mod + +def attributes(full_attribute_name): + """Load a module and retrieve an attribute of that module.""" + + # Parse out the path, module, and attribute + last_dot = full_attribute_name.rfind(".") + attr_name = full_attribute_name[last_dot + 1:] + mod_path = full_attribute_name[:last_dot] + + mod = modules(mod_path) + # Let an AttributeError propagate outward. + try: + attr = getattr(mod, attr_name) + except AttributeError: + raise AttributeError("'%s' object has no attribute '%s'" + % (mod_path, attr_name)) + + # Return a reference to the attribute. + return attr + + diff --git a/libs/cherrypy/lib/sessions.py b/libs/cherrypy/lib/sessions.py new file mode 100644 index 0000000..9763f12 --- /dev/null +++ b/libs/cherrypy/lib/sessions.py @@ -0,0 +1,871 @@ +"""Session implementation for CherryPy. + +You need to edit your config file to use sessions. Here's an example:: + + [/] + tools.sessions.on = True + tools.sessions.storage_type = "file" + tools.sessions.storage_path = "/home/site/sessions" + tools.sessions.timeout = 60 + +This sets the session to be stored in files in the directory /home/site/sessions, +and the session timeout to 60 minutes. If you omit ``storage_type`` the sessions +will be saved in RAM. ``tools.sessions.on`` is the only required line for +working sessions, the rest are optional. + +By default, the session ID is passed in a cookie, so the client's browser must +have cookies enabled for your site. + +To set data for the current session, use +``cherrypy.session['fieldname'] = 'fieldvalue'``; +to get data use ``cherrypy.session.get('fieldname')``. + +================ +Locking sessions +================ + +By default, the ``'locking'`` mode of sessions is ``'implicit'``, which means +the session is locked early and unlocked late. If you want to control when the +session data is locked and unlocked, set ``tools.sessions.locking = 'explicit'``. +Then call ``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_lock()``. +Regardless of which mode you use, the session is guaranteed to be unlocked when +the request is complete. + +================= +Expiring Sessions +================= + +You can force a session to expire with :func:`cherrypy.lib.sessions.expire`. +Simply call that function at the point you want the session to expire, and it +will cause the session cookie to expire client-side. + +=========================== +Session Fixation Protection +=========================== + +If CherryPy receives, via a request cookie, a session id that it does not +recognize, it will reject that id and create a new one to return in the +response cookie. This `helps prevent session fixation attacks +`_. +However, CherryPy "recognizes" a session id by looking up the saved session +data for that id. Therefore, if you never save any session data, +**you will get a new session id for every request**. + +================ +Sharing Sessions +================ + +If you run multiple instances of CherryPy (for example via mod_python behind +Apache prefork), you most likely cannot use the RAM session backend, since each +instance of CherryPy will have its own memory space. Use a different backend +instead, and verify that all instances are pointing at the same file or db +location. Alternately, you might try a load balancer which makes sessions +"sticky". Google is your friend, there. + +================ +Expiration Dates +================ + +The response cookie will possess an expiration date to inform the client at +which point to stop sending the cookie back in requests. If the server time +and client time differ, expect sessions to be unreliable. **Make sure the +system time of your server is accurate**. + +CherryPy defaults to a 60-minute session timeout, which also applies to the +cookie which is sent to the client. Unfortunately, some versions of Safari +("4 public beta" on Windows XP at least) appear to have a bug in their parsing +of the GMT expiration date--they appear to interpret the date as one hour in +the past. Sixty minutes minus one hour is pretty close to zero, so you may +experience this bug as a new session id for every request, unless the requests +are less than one second apart. To fix, try increasing the session.timeout. + +On the other extreme, some users report Firefox sending cookies after their +expiration date, although this was on a system with an inaccurate system time. +Maybe FF doesn't trust system time. +""" + +import datetime +import os +import random +import time +import threading +import types +from warnings import warn + +import cherrypy +from cherrypy._cpcompat import copyitems, pickle, random20, unicodestr +from cherrypy.lib import httputil + + +missing = object() + +class Session(object): + """A CherryPy dict-like Session object (one per request).""" + + _id = None + + id_observers = None + "A list of callbacks to which to pass new id's." + + def _get_id(self): + return self._id + def _set_id(self, value): + self._id = value + for o in self.id_observers: + o(value) + id = property(_get_id, _set_id, doc="The current session ID.") + + timeout = 60 + "Number of minutes after which to delete session data." + + locked = False + """ + If True, this session instance has exclusive read/write access + to session data.""" + + loaded = False + """ + If True, data has been retrieved from storage. This should happen + automatically on the first attempt to access session data.""" + + clean_thread = None + "Class-level Monitor which calls self.clean_up." + + clean_freq = 5 + "The poll rate for expired session cleanup in minutes." + + originalid = None + "The session id passed by the client. May be missing or unsafe." + + missing = False + "True if the session requested by the client did not exist." + + regenerated = False + """ + True if the application called session.regenerate(). This is not set by + internal calls to regenerate the session id.""" + + debug=False + + def __init__(self, id=None, **kwargs): + self.id_observers = [] + self._data = {} + + for k, v in kwargs.items(): + setattr(self, k, v) + + self.originalid = id + self.missing = False + if id is None: + if self.debug: + cherrypy.log('No id given; making a new one', 'TOOLS.SESSIONS') + self._regenerate() + else: + self.id = id + if not self._exists(): + if self.debug: + cherrypy.log('Expired or malicious session %r; ' + 'making a new one' % id, 'TOOLS.SESSIONS') + # Expired or malicious session. Make a new one. + # See http://www.cherrypy.org/ticket/709. + self.id = None + self.missing = True + self._regenerate() + + def now(self): + """Generate the session specific concept of 'now'. + + Other session providers can override this to use alternative, + possibly timezone aware, versions of 'now'. + """ + return datetime.datetime.now() + + def regenerate(self): + """Replace the current session (with a new id).""" + self.regenerated = True + self._regenerate() + + def _regenerate(self): + if self.id is not None: + self.delete() + + old_session_was_locked = self.locked + if old_session_was_locked: + self.release_lock() + + self.id = None + while self.id is None: + self.id = self.generate_id() + # Assert that the generated id is not already stored. + if self._exists(): + self.id = None + + if old_session_was_locked: + self.acquire_lock() + + def clean_up(self): + """Clean up expired sessions.""" + pass + + def generate_id(self): + """Return a new session id.""" + return random20() + + def save(self): + """Save session data.""" + try: + # If session data has never been loaded then it's never been + # accessed: no need to save it + if self.loaded: + t = datetime.timedelta(seconds = self.timeout * 60) + expiration_time = self.now() + t + if self.debug: + cherrypy.log('Saving with expiry %s' % expiration_time, + 'TOOLS.SESSIONS') + self._save(expiration_time) + + finally: + if self.locked: + # Always release the lock if the user didn't release it + self.release_lock() + + def load(self): + """Copy stored session data into this session instance.""" + data = self._load() + # data is either None or a tuple (session_data, expiration_time) + if data is None or data[1] < self.now(): + if self.debug: + cherrypy.log('Expired session, flushing data', 'TOOLS.SESSIONS') + self._data = {} + else: + self._data = data[0] + self.loaded = True + + # Stick the clean_thread in the class, not the instance. + # The instances are created and destroyed per-request. + cls = self.__class__ + if self.clean_freq and not cls.clean_thread: + # clean_up is in instancemethod and not a classmethod, + # so that tool config can be accessed inside the method. + t = cherrypy.process.plugins.Monitor( + cherrypy.engine, self.clean_up, self.clean_freq * 60, + name='Session cleanup') + t.subscribe() + cls.clean_thread = t + t.start() + + def delete(self): + """Delete stored session data.""" + self._delete() + + def __getitem__(self, key): + if not self.loaded: self.load() + return self._data[key] + + def __setitem__(self, key, value): + if not self.loaded: self.load() + self._data[key] = value + + def __delitem__(self, key): + if not self.loaded: self.load() + del self._data[key] + + def pop(self, key, default=missing): + """Remove the specified key and return the corresponding value. + If key is not found, default is returned if given, + otherwise KeyError is raised. + """ + if not self.loaded: self.load() + if default is missing: + return self._data.pop(key) + else: + return self._data.pop(key, default) + + def __contains__(self, key): + if not self.loaded: self.load() + return key in self._data + + if hasattr({}, 'has_key'): + def has_key(self, key): + """D.has_key(k) -> True if D has a key k, else False.""" + if not self.loaded: self.load() + return key in self._data + + def get(self, key, default=None): + """D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.""" + if not self.loaded: self.load() + return self._data.get(key, default) + + def update(self, d): + """D.update(E) -> None. Update D from E: for k in E: D[k] = E[k].""" + if not self.loaded: self.load() + self._data.update(d) + + def setdefault(self, key, default=None): + """D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D.""" + if not self.loaded: self.load() + return self._data.setdefault(key, default) + + def clear(self): + """D.clear() -> None. Remove all items from D.""" + if not self.loaded: self.load() + self._data.clear() + + def keys(self): + """D.keys() -> list of D's keys.""" + if not self.loaded: self.load() + return self._data.keys() + + def items(self): + """D.items() -> list of D's (key, value) pairs, as 2-tuples.""" + if not self.loaded: self.load() + return self._data.items() + + def values(self): + """D.values() -> list of D's values.""" + if not self.loaded: self.load() + return self._data.values() + + +class RamSession(Session): + + # Class-level objects. Don't rebind these! + cache = {} + locks = {} + + def clean_up(self): + """Clean up expired sessions.""" + now = self.now() + for id, (data, expiration_time) in copyitems(self.cache): + if expiration_time <= now: + try: + del self.cache[id] + except KeyError: + pass + try: + del self.locks[id] + except KeyError: + pass + + # added to remove obsolete lock objects + for id in list(self.locks): + if id not in self.cache: + self.locks.pop(id, None) + + def _exists(self): + return self.id in self.cache + + def _load(self): + return self.cache.get(self.id) + + def _save(self, expiration_time): + self.cache[self.id] = (self._data, expiration_time) + + def _delete(self): + self.cache.pop(self.id, None) + + def acquire_lock(self): + """Acquire an exclusive lock on the currently-loaded session data.""" + self.locked = True + self.locks.setdefault(self.id, threading.RLock()).acquire() + + def release_lock(self): + """Release the lock on the currently-loaded session data.""" + self.locks[self.id].release() + self.locked = False + + def __len__(self): + """Return the number of active sessions.""" + return len(self.cache) + + +class FileSession(Session): + """Implementation of the File backend for sessions + + storage_path + The folder where session data will be saved. Each session + will be saved as pickle.dump(data, expiration_time) in its own file; + the filename will be self.SESSION_PREFIX + self.id. + + """ + + SESSION_PREFIX = 'session-' + LOCK_SUFFIX = '.lock' + pickle_protocol = pickle.HIGHEST_PROTOCOL + + def __init__(self, id=None, **kwargs): + # The 'storage_path' arg is required for file-based sessions. + kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) + Session.__init__(self, id=id, **kwargs) + + def setup(cls, **kwargs): + """Set up the storage system for file-based sessions. + + This should only be called once per process; this will be done + automatically when using sessions.init (as the built-in Tool does). + """ + # The 'storage_path' arg is required for file-based sessions. + kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) + + for k, v in kwargs.items(): + setattr(cls, k, v) + + # Warn if any lock files exist at startup. + lockfiles = [fname for fname in os.listdir(cls.storage_path) + if (fname.startswith(cls.SESSION_PREFIX) + and fname.endswith(cls.LOCK_SUFFIX))] + if lockfiles: + plural = ('', 's')[len(lockfiles) > 1] + warn("%s session lockfile%s found at startup. If you are " + "only running one process, then you may need to " + "manually delete the lockfiles found at %r." + % (len(lockfiles), plural, cls.storage_path)) + setup = classmethod(setup) + + def _get_file_path(self): + f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id) + if not os.path.abspath(f).startswith(self.storage_path): + raise cherrypy.HTTPError(400, "Invalid session id in cookie.") + return f + + def _exists(self): + path = self._get_file_path() + return os.path.exists(path) + + def _load(self, path=None): + if path is None: + path = self._get_file_path() + try: + f = open(path, "rb") + try: + return pickle.load(f) + finally: + f.close() + except (IOError, EOFError): + return None + + def _save(self, expiration_time): + f = open(self._get_file_path(), "wb") + try: + pickle.dump((self._data, expiration_time), f, self.pickle_protocol) + finally: + f.close() + + def _delete(self): + try: + os.unlink(self._get_file_path()) + except OSError: + pass + + def acquire_lock(self, path=None): + """Acquire an exclusive lock on the currently-loaded session data.""" + if path is None: + path = self._get_file_path() + path += self.LOCK_SUFFIX + while True: + try: + lockfd = os.open(path, os.O_CREAT|os.O_WRONLY|os.O_EXCL) + except OSError: + time.sleep(0.1) + else: + os.close(lockfd) + break + self.locked = True + + def release_lock(self, path=None): + """Release the lock on the currently-loaded session data.""" + if path is None: + path = self._get_file_path() + os.unlink(path + self.LOCK_SUFFIX) + self.locked = False + + def clean_up(self): + """Clean up expired sessions.""" + now = self.now() + # Iterate over all session files in self.storage_path + for fname in os.listdir(self.storage_path): + if (fname.startswith(self.SESSION_PREFIX) + and not fname.endswith(self.LOCK_SUFFIX)): + # We have a session file: lock and load it and check + # if it's expired. If it fails, nevermind. + path = os.path.join(self.storage_path, fname) + self.acquire_lock(path) + try: + contents = self._load(path) + # _load returns None on IOError + if contents is not None: + data, expiration_time = contents + if expiration_time < now: + # Session expired: deleting it + os.unlink(path) + finally: + self.release_lock(path) + + def __len__(self): + """Return the number of active sessions.""" + return len([fname for fname in os.listdir(self.storage_path) + if (fname.startswith(self.SESSION_PREFIX) + and not fname.endswith(self.LOCK_SUFFIX))]) + + +class PostgresqlSession(Session): + """ Implementation of the PostgreSQL backend for sessions. It assumes + a table like this:: + + create table session ( + id varchar(40), + data text, + expiration_time timestamp + ) + + You must provide your own get_db function. + """ + + pickle_protocol = pickle.HIGHEST_PROTOCOL + + def __init__(self, id=None, **kwargs): + Session.__init__(self, id, **kwargs) + self.cursor = self.db.cursor() + + def setup(cls, **kwargs): + """Set up the storage system for Postgres-based sessions. + + This should only be called once per process; this will be done + automatically when using sessions.init (as the built-in Tool does). + """ + for k, v in kwargs.items(): + setattr(cls, k, v) + + self.db = self.get_db() + setup = classmethod(setup) + + def __del__(self): + if self.cursor: + self.cursor.close() + self.db.commit() + + def _exists(self): + # Select session data from table + self.cursor.execute('select data, expiration_time from session ' + 'where id=%s', (self.id,)) + rows = self.cursor.fetchall() + return bool(rows) + + def _load(self): + # Select session data from table + self.cursor.execute('select data, expiration_time from session ' + 'where id=%s', (self.id,)) + rows = self.cursor.fetchall() + if not rows: + return None + + pickled_data, expiration_time = rows[0] + data = pickle.loads(pickled_data) + return data, expiration_time + + def _save(self, expiration_time): + pickled_data = pickle.dumps(self._data, self.pickle_protocol) + self.cursor.execute('update session set data = %s, ' + 'expiration_time = %s where id = %s', + (pickled_data, expiration_time, self.id)) + + def _delete(self): + self.cursor.execute('delete from session where id=%s', (self.id,)) + + def acquire_lock(self): + """Acquire an exclusive lock on the currently-loaded session data.""" + # We use the "for update" clause to lock the row + self.locked = True + self.cursor.execute('select id from session where id=%s for update', + (self.id,)) + + def release_lock(self): + """Release the lock on the currently-loaded session data.""" + # We just close the cursor and that will remove the lock + # introduced by the "for update" clause + self.cursor.close() + self.locked = False + + def clean_up(self): + """Clean up expired sessions.""" + self.cursor.execute('delete from session where expiration_time < %s', + (self.now(),)) + + +class MemcachedSession(Session): + + # The most popular memcached client for Python isn't thread-safe. + # Wrap all .get and .set operations in a single lock. + mc_lock = threading.RLock() + + # This is a seperate set of locks per session id. + locks = {} + + servers = ['127.0.0.1:11211'] + + def setup(cls, **kwargs): + """Set up the storage system for memcached-based sessions. + + This should only be called once per process; this will be done + automatically when using sessions.init (as the built-in Tool does). + """ + for k, v in kwargs.items(): + setattr(cls, k, v) + + import memcache + cls.cache = memcache.Client(cls.servers) + setup = classmethod(setup) + + def _get_id(self): + return self._id + def _set_id(self, value): + # This encode() call is where we differ from the superclass. + # Memcache keys MUST be byte strings, not unicode. + if isinstance(value, unicodestr): + value = value.encode('utf-8') + + self._id = value + for o in self.id_observers: + o(value) + id = property(_get_id, _set_id, doc="The current session ID.") + + def _exists(self): + self.mc_lock.acquire() + try: + return bool(self.cache.get(self.id)) + finally: + self.mc_lock.release() + + def _load(self): + self.mc_lock.acquire() + try: + return self.cache.get(self.id) + finally: + self.mc_lock.release() + + def _save(self, expiration_time): + # Send the expiration time as "Unix time" (seconds since 1/1/1970) + td = int(time.mktime(expiration_time.timetuple())) + self.mc_lock.acquire() + try: + if not self.cache.set(self.id, (self._data, expiration_time), td): + raise AssertionError("Session data for id %r not set." % self.id) + finally: + self.mc_lock.release() + + def _delete(self): + self.cache.delete(self.id) + + def acquire_lock(self): + """Acquire an exclusive lock on the currently-loaded session data.""" + self.locked = True + self.locks.setdefault(self.id, threading.RLock()).acquire() + + def release_lock(self): + """Release the lock on the currently-loaded session data.""" + self.locks[self.id].release() + self.locked = False + + def __len__(self): + """Return the number of active sessions.""" + raise NotImplementedError + + +# Hook functions (for CherryPy tools) + +def save(): + """Save any changed session data.""" + + if not hasattr(cherrypy.serving, "session"): + return + request = cherrypy.serving.request + response = cherrypy.serving.response + + # Guard against running twice + if hasattr(request, "_sessionsaved"): + return + request._sessionsaved = True + + if response.stream: + # If the body is being streamed, we have to save the data + # *after* the response has been written out + request.hooks.attach('on_end_request', cherrypy.session.save) + else: + # If the body is not being streamed, we save the data now + # (so we can release the lock). + if isinstance(response.body, types.GeneratorType): + response.collapse_body() + cherrypy.session.save() +save.failsafe = True + +def close(): + """Close the session object for this request.""" + sess = getattr(cherrypy.serving, "session", None) + if getattr(sess, "locked", False): + # If the session is still locked we release the lock + sess.release_lock() +close.failsafe = True +close.priority = 90 + + +def init(storage_type='ram', path=None, path_header=None, name='session_id', + timeout=60, domain=None, secure=False, clean_freq=5, + persistent=True, httponly=False, debug=False, **kwargs): + """Initialize session object (using cookies). + + storage_type + One of 'ram', 'file', 'postgresql', 'memcached'. This will be + used to look up the corresponding class in cherrypy.lib.sessions + globals. For example, 'file' will use the FileSession class. + + path + The 'path' value to stick in the response cookie metadata. + + path_header + If 'path' is None (the default), then the response + cookie 'path' will be pulled from request.headers[path_header]. + + name + The name of the cookie. + + timeout + The expiration timeout (in minutes) for the stored session data. + If 'persistent' is True (the default), this is also the timeout + for the cookie. + + domain + The cookie domain. + + secure + If False (the default) the cookie 'secure' value will not + be set. If True, the cookie 'secure' value will be set (to 1). + + clean_freq (minutes) + The poll rate for expired session cleanup. + + persistent + If True (the default), the 'timeout' argument will be used + to expire the cookie. If False, the cookie will not have an expiry, + and the cookie will be a "session cookie" which expires when the + browser is closed. + + httponly + If False (the default) the cookie 'httponly' value will not be set. + If True, the cookie 'httponly' value will be set (to 1). + + Any additional kwargs will be bound to the new Session instance, + and may be specific to the storage type. See the subclass of Session + you're using for more information. + """ + + request = cherrypy.serving.request + + # Guard against running twice + if hasattr(request, "_session_init_flag"): + return + request._session_init_flag = True + + # Check if request came with a session ID + id = None + if name in request.cookie: + id = request.cookie[name].value + if debug: + cherrypy.log('ID obtained from request.cookie: %r' % id, + 'TOOLS.SESSIONS') + + # Find the storage class and call setup (first time only). + storage_class = storage_type.title() + 'Session' + storage_class = globals()[storage_class] + if not hasattr(cherrypy, "session"): + if hasattr(storage_class, "setup"): + storage_class.setup(**kwargs) + + # Create and attach a new Session instance to cherrypy.serving. + # It will possess a reference to (and lock, and lazily load) + # the requested session data. + kwargs['timeout'] = timeout + kwargs['clean_freq'] = clean_freq + cherrypy.serving.session = sess = storage_class(id, **kwargs) + sess.debug = debug + def update_cookie(id): + """Update the cookie every time the session id changes.""" + cherrypy.serving.response.cookie[name] = id + sess.id_observers.append(update_cookie) + + # Create cherrypy.session which will proxy to cherrypy.serving.session + if not hasattr(cherrypy, "session"): + cherrypy.session = cherrypy._ThreadLocalProxy('session') + + if persistent: + cookie_timeout = timeout + else: + # See http://support.microsoft.com/kb/223799/EN-US/ + # and http://support.mozilla.com/en-US/kb/Cookies + cookie_timeout = None + set_response_cookie(path=path, path_header=path_header, name=name, + timeout=cookie_timeout, domain=domain, secure=secure, + httponly=httponly) + + +def set_response_cookie(path=None, path_header=None, name='session_id', + timeout=60, domain=None, secure=False, httponly=False): + """Set a response cookie for the client. + + path + the 'path' value to stick in the response cookie metadata. + + path_header + if 'path' is None (the default), then the response + cookie 'path' will be pulled from request.headers[path_header]. + + name + the name of the cookie. + + timeout + the expiration timeout for the cookie. If 0 or other boolean + False, no 'expires' param will be set, and the cookie will be a + "session cookie" which expires when the browser is closed. + + domain + the cookie domain. + + secure + if False (the default) the cookie 'secure' value will not + be set. If True, the cookie 'secure' value will be set (to 1). + + httponly + If False (the default) the cookie 'httponly' value will not be set. + If True, the cookie 'httponly' value will be set (to 1). + + """ + # Set response cookie + cookie = cherrypy.serving.response.cookie + cookie[name] = cherrypy.serving.session.id + cookie[name]['path'] = (path or cherrypy.serving.request.headers.get(path_header) + or '/') + + # We'd like to use the "max-age" param as indicated in + # http://www.faqs.org/rfcs/rfc2109.html but IE doesn't + # save it to disk and the session is lost if people close + # the browser. So we have to use the old "expires" ... sigh ... +## cookie[name]['max-age'] = timeout * 60 + if timeout: + e = time.time() + (timeout * 60) + cookie[name]['expires'] = httputil.HTTPDate(e) + if domain is not None: + cookie[name]['domain'] = domain + if secure: + cookie[name]['secure'] = 1 + if httponly: + if not cookie[name].isReservedKey('httponly'): + raise ValueError("The httponly cookie token is not supported.") + cookie[name]['httponly'] = 1 + +def expire(): + """Expire the current session cookie.""" + name = cherrypy.serving.request.config.get('tools.sessions.name', 'session_id') + one_year = 60 * 60 * 24 * 365 + e = time.time() - one_year + cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e) + + diff --git a/libs/cherrypy/lib/static.py b/libs/cherrypy/lib/static.py new file mode 100644 index 0000000..2d14230 --- /dev/null +++ b/libs/cherrypy/lib/static.py @@ -0,0 +1,363 @@ +try: + from io import UnsupportedOperation +except ImportError: + UnsupportedOperation = object() +import logging +import mimetypes +mimetypes.init() +mimetypes.types_map['.dwg']='image/x-dwg' +mimetypes.types_map['.ico']='image/x-icon' +mimetypes.types_map['.bz2']='application/x-bzip2' +mimetypes.types_map['.gz']='application/x-gzip' + +import os +import re +import stat +import time + +import cherrypy +from cherrypy._cpcompat import ntob, unquote +from cherrypy.lib import cptools, httputil, file_generator_limited + + +def serve_file(path, content_type=None, disposition=None, name=None, debug=False): + """Set status, headers, and body in order to serve the given path. + + The Content-Type header will be set to the content_type arg, if provided. + If not provided, the Content-Type will be guessed by the file extension + of the 'path' argument. + + If disposition is not None, the Content-Disposition header will be set + to "; filename=". If name is None, it will be set + to the basename of path. If disposition is None, no Content-Disposition + header will be written. + """ + + response = cherrypy.serving.response + + # If path is relative, users should fix it by making path absolute. + # That is, CherryPy should not guess where the application root is. + # It certainly should *not* use cwd (since CP may be invoked from a + # variety of paths). If using tools.staticdir, you can make your relative + # paths become absolute by supplying a value for "tools.staticdir.root". + if not os.path.isabs(path): + msg = "'%s' is not an absolute path." % path + if debug: + cherrypy.log(msg, 'TOOLS.STATICFILE') + raise ValueError(msg) + + try: + st = os.stat(path) + except OSError: + if debug: + cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC') + raise cherrypy.NotFound() + + # Check if path is a directory. + if stat.S_ISDIR(st.st_mode): + # Let the caller deal with it as they like. + if debug: + cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC') + raise cherrypy.NotFound() + + # Set the Last-Modified response header, so that + # modified-since validation code can work. + response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime) + cptools.validate_since() + + if content_type is None: + # Set content-type based on filename extension + ext = "" + i = path.rfind('.') + if i != -1: + ext = path[i:].lower() + content_type = mimetypes.types_map.get(ext, None) + if content_type is not None: + response.headers['Content-Type'] = content_type + if debug: + cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC') + + cd = None + if disposition is not None: + if name is None: + name = os.path.basename(path) + cd = '%s; filename="%s"' % (disposition, name) + response.headers["Content-Disposition"] = cd + if debug: + cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') + + # Set Content-Length and use an iterable (file object) + # this way CP won't load the whole file in memory + content_length = st.st_size + fileobj = open(path, 'rb') + return _serve_fileobj(fileobj, content_type, content_length, debug=debug) + +def serve_fileobj(fileobj, content_type=None, disposition=None, name=None, + debug=False): + """Set status, headers, and body in order to serve the given file object. + + The Content-Type header will be set to the content_type arg, if provided. + + If disposition is not None, the Content-Disposition header will be set + to "; filename=". If name is None, 'filename' will + not be set. If disposition is None, no Content-Disposition header will + be written. + + CAUTION: If the request contains a 'Range' header, one or more seek()s will + be performed on the file object. This may cause undesired behavior if + the file object is not seekable. It could also produce undesired results + if the caller set the read position of the file object prior to calling + serve_fileobj(), expecting that the data would be served starting from that + position. + """ + + response = cherrypy.serving.response + + try: + st = os.fstat(fileobj.fileno()) + except AttributeError: + if debug: + cherrypy.log('os has no fstat attribute', 'TOOLS.STATIC') + content_length = None + except UnsupportedOperation: + content_length = None + else: + # Set the Last-Modified response header, so that + # modified-since validation code can work. + response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime) + cptools.validate_since() + content_length = st.st_size + + if content_type is not None: + response.headers['Content-Type'] = content_type + if debug: + cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC') + + cd = None + if disposition is not None: + if name is None: + cd = disposition + else: + cd = '%s; filename="%s"' % (disposition, name) + response.headers["Content-Disposition"] = cd + if debug: + cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') + + return _serve_fileobj(fileobj, content_type, content_length, debug=debug) + +def _serve_fileobj(fileobj, content_type, content_length, debug=False): + """Internal. Set response.body to the given file object, perhaps ranged.""" + response = cherrypy.serving.response + + # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code + request = cherrypy.serving.request + if request.protocol >= (1, 1): + response.headers["Accept-Ranges"] = "bytes" + r = httputil.get_ranges(request.headers.get('Range'), content_length) + if r == []: + response.headers['Content-Range'] = "bytes */%s" % content_length + message = "Invalid Range (first-byte-pos greater than Content-Length)" + if debug: + cherrypy.log(message, 'TOOLS.STATIC') + raise cherrypy.HTTPError(416, message) + + if r: + if len(r) == 1: + # Return a single-part response. + start, stop = r[0] + if stop > content_length: + stop = content_length + r_len = stop - start + if debug: + cherrypy.log('Single part; start: %r, stop: %r' % (start, stop), + 'TOOLS.STATIC') + response.status = "206 Partial Content" + response.headers['Content-Range'] = ( + "bytes %s-%s/%s" % (start, stop - 1, content_length)) + response.headers['Content-Length'] = r_len + fileobj.seek(start) + response.body = file_generator_limited(fileobj, r_len) + else: + # Return a multipart/byteranges response. + response.status = "206 Partial Content" + try: + # Python 3 + from email.generator import _make_boundary as choose_boundary + except ImportError: + # Python 2 + from mimetools import choose_boundary + boundary = choose_boundary() + ct = "multipart/byteranges; boundary=%s" % boundary + response.headers['Content-Type'] = ct + if "Content-Length" in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers["Content-Length"] + + def file_ranges(): + # Apache compatibility: + yield ntob("\r\n") + + for start, stop in r: + if debug: + cherrypy.log('Multipart; start: %r, stop: %r' % (start, stop), + 'TOOLS.STATIC') + yield ntob("--" + boundary, 'ascii') + yield ntob("\r\nContent-type: %s" % content_type, 'ascii') + yield ntob("\r\nContent-range: bytes %s-%s/%s\r\n\r\n" + % (start, stop - 1, content_length), 'ascii') + fileobj.seek(start) + for chunk in file_generator_limited(fileobj, stop-start): + yield chunk + yield ntob("\r\n") + # Final boundary + yield ntob("--" + boundary + "--", 'ascii') + + # Apache compatibility: + yield ntob("\r\n") + response.body = file_ranges() + return response.body + else: + if debug: + cherrypy.log('No byteranges requested', 'TOOLS.STATIC') + + # Set Content-Length and use an iterable (file object) + # this way CP won't load the whole file in memory + response.headers['Content-Length'] = content_length + response.body = fileobj + return response.body + +def serve_download(path, name=None): + """Serve 'path' as an application/x-download attachment.""" + # This is such a common idiom I felt it deserved its own wrapper. + return serve_file(path, "application/x-download", "attachment", name) + + +def _attempt(filename, content_types, debug=False): + if debug: + cherrypy.log('Attempting %r (content_types %r)' % + (filename, content_types), 'TOOLS.STATICDIR') + try: + # you can set the content types for a + # complete directory per extension + content_type = None + if content_types: + r, ext = os.path.splitext(filename) + content_type = content_types.get(ext[1:], None) + serve_file(filename, content_type=content_type, debug=debug) + return True + except cherrypy.NotFound: + # If we didn't find the static file, continue handling the + # request. We might find a dynamic handler instead. + if debug: + cherrypy.log('NotFound', 'TOOLS.STATICFILE') + return False + +def staticdir(section, dir, root="", match="", content_types=None, index="", + debug=False): + """Serve a static resource from the given (root +) dir. + + match + If given, request.path_info will be searched for the given + regular expression before attempting to serve static content. + + content_types + If given, it should be a Python dictionary of + {file-extension: content-type} pairs, where 'file-extension' is + a string (e.g. "gif") and 'content-type' is the value to write + out in the Content-Type response header (e.g. "image/gif"). + + index + If provided, it should be the (relative) name of a file to + serve for directory requests. For example, if the dir argument is + '/home/me', the Request-URI is 'myapp', and the index arg is + 'index.html', the file '/home/me/myapp/index.html' will be sought. + """ + request = cherrypy.serving.request + if request.method not in ('GET', 'HEAD'): + if debug: + cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR') + return False + + if match and not re.search(match, request.path_info): + if debug: + cherrypy.log('request.path_info %r does not match pattern %r' % + (request.path_info, match), 'TOOLS.STATICDIR') + return False + + # Allow the use of '~' to refer to a user's home directory. + dir = os.path.expanduser(dir) + + # If dir is relative, make absolute using "root". + if not os.path.isabs(dir): + if not root: + msg = "Static dir requires an absolute dir (or root)." + if debug: + cherrypy.log(msg, 'TOOLS.STATICDIR') + raise ValueError(msg) + dir = os.path.join(root, dir) + + # Determine where we are in the object tree relative to 'section' + # (where the static tool was defined). + if section == 'global': + section = "/" + section = section.rstrip(r"\/") + branch = request.path_info[len(section) + 1:] + branch = unquote(branch.lstrip(r"\/")) + + # If branch is "", filename will end in a slash + filename = os.path.join(dir, branch) + if debug: + cherrypy.log('Checking file %r to fulfill %r' % + (filename, request.path_info), 'TOOLS.STATICDIR') + + # There's a chance that the branch pulled from the URL might + # have ".." or similar uplevel attacks in it. Check that the final + # filename is a child of dir. + if not os.path.normpath(filename).startswith(os.path.normpath(dir)): + raise cherrypy.HTTPError(403) # Forbidden + + handled = _attempt(filename, content_types) + if not handled: + # Check for an index file if a folder was requested. + if index: + handled = _attempt(os.path.join(filename, index), content_types) + if handled: + request.is_index = filename[-1] in (r"\/") + return handled + +def staticfile(filename, root=None, match="", content_types=None, debug=False): + """Serve a static resource from the given (root +) filename. + + match + If given, request.path_info will be searched for the given + regular expression before attempting to serve static content. + + content_types + If given, it should be a Python dictionary of + {file-extension: content-type} pairs, where 'file-extension' is + a string (e.g. "gif") and 'content-type' is the value to write + out in the Content-Type response header (e.g. "image/gif"). + + """ + request = cherrypy.serving.request + if request.method not in ('GET', 'HEAD'): + if debug: + cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE') + return False + + if match and not re.search(match, request.path_info): + if debug: + cherrypy.log('request.path_info %r does not match pattern %r' % + (request.path_info, match), 'TOOLS.STATICFILE') + return False + + # If filename is relative, make absolute using "root". + if not os.path.isabs(filename): + if not root: + msg = "Static tool requires an absolute filename (got '%s')." % filename + if debug: + cherrypy.log(msg, 'TOOLS.STATICFILE') + raise ValueError(msg) + filename = os.path.join(root, filename) + + return _attempt(filename, content_types, debug=debug) diff --git a/libs/cherrypy/lib/xmlrpcutil.py b/libs/cherrypy/lib/xmlrpcutil.py new file mode 100644 index 0000000..9a44464 --- /dev/null +++ b/libs/cherrypy/lib/xmlrpcutil.py @@ -0,0 +1,55 @@ +import sys + +import cherrypy +from cherrypy._cpcompat import ntob + +def get_xmlrpclib(): + try: + import xmlrpc.client as x + except ImportError: + import xmlrpclib as x + return x + +def process_body(): + """Return (params, method) from request body.""" + try: + return get_xmlrpclib().loads(cherrypy.request.body.read()) + except Exception: + return ('ERROR PARAMS', ), 'ERRORMETHOD' + + +def patched_path(path): + """Return 'path', doctored for RPC.""" + if not path.endswith('/'): + path += '/' + if path.startswith('/RPC2/'): + # strip the first /rpc2 + path = path[5:] + return path + + +def _set_response(body): + # The XML-RPC spec (http://www.xmlrpc.com/spec) says: + # "Unless there's a lower-level error, always return 200 OK." + # Since Python's xmlrpclib interprets a non-200 response + # as a "Protocol Error", we'll just return 200 every time. + response = cherrypy.response + response.status = '200 OK' + response.body = ntob(body, 'utf-8') + response.headers['Content-Type'] = 'text/xml' + response.headers['Content-Length'] = len(body) + + +def respond(body, encoding='utf-8', allow_none=0): + xmlrpclib = get_xmlrpclib() + if not isinstance(body, xmlrpclib.Fault): + body = (body,) + _set_response(xmlrpclib.dumps(body, methodresponse=1, + encoding=encoding, + allow_none=allow_none)) + +def on_error(*args, **kwargs): + body = str(sys.exc_info()[1]) + xmlrpclib = get_xmlrpclib() + _set_response(xmlrpclib.dumps(xmlrpclib.Fault(1, body))) + diff --git a/libs/cherrypy/process/__init__.py b/libs/cherrypy/process/__init__.py new file mode 100644 index 0000000..f15b123 --- /dev/null +++ b/libs/cherrypy/process/__init__.py @@ -0,0 +1,14 @@ +"""Site container for an HTTP server. + +A Web Site Process Bus object is used to connect applications, servers, +and frameworks with site-wide services such as daemonization, process +reload, signal handling, drop privileges, PID file management, logging +for all of these, and many more. + +The 'plugins' module defines a few abstract and concrete services for +use with the bus. Some use tool-specific channels; see the documentation +for each class. +""" + +from cherrypy.process.wspbus import bus +from cherrypy.process import plugins, servers diff --git a/libs/cherrypy/process/plugins.py b/libs/cherrypy/process/plugins.py new file mode 100644 index 0000000..ba618a0 --- /dev/null +++ b/libs/cherrypy/process/plugins.py @@ -0,0 +1,683 @@ +"""Site services for use with a Web Site Process Bus.""" + +import os +import re +import signal as _signal +import sys +import time +import threading + +from cherrypy._cpcompat import basestring, get_daemon, get_thread_ident, ntob, set + +# _module__file__base is used by Autoreload to make +# absolute any filenames retrieved from sys.modules which are not +# already absolute paths. This is to work around Python's quirk +# of importing the startup script and using a relative filename +# for it in sys.modules. +# +# Autoreload examines sys.modules afresh every time it runs. If an application +# changes the current directory by executing os.chdir(), then the next time +# Autoreload runs, it will not be able to find any filenames which are +# not absolute paths, because the current directory is not the same as when the +# module was first imported. Autoreload will then wrongly conclude the file has +# "changed", and initiate the shutdown/re-exec sequence. +# See ticket #917. +# For this workaround to have a decent probability of success, this module +# needs to be imported as early as possible, before the app has much chance +# to change the working directory. +_module__file__base = os.getcwd() + + +class SimplePlugin(object): + """Plugin base class which auto-subscribes methods for known channels.""" + + bus = None + """A :class:`Bus `, usually cherrypy.engine.""" + + def __init__(self, bus): + self.bus = bus + + def subscribe(self): + """Register this object as a (multi-channel) listener on the bus.""" + for channel in self.bus.listeners: + # Subscribe self.start, self.exit, etc. if present. + method = getattr(self, channel, None) + if method is not None: + self.bus.subscribe(channel, method) + + def unsubscribe(self): + """Unregister this object as a listener on the bus.""" + for channel in self.bus.listeners: + # Unsubscribe self.start, self.exit, etc. if present. + method = getattr(self, channel, None) + if method is not None: + self.bus.unsubscribe(channel, method) + + + +class SignalHandler(object): + """Register bus channels (and listeners) for system signals. + + You can modify what signals your application listens for, and what it does + when it receives signals, by modifying :attr:`SignalHandler.handlers`, + a dict of {signal name: callback} pairs. The default set is:: + + handlers = {'SIGTERM': self.bus.exit, + 'SIGHUP': self.handle_SIGHUP, + 'SIGUSR1': self.bus.graceful, + } + + The :func:`SignalHandler.handle_SIGHUP`` method calls + :func:`bus.restart()` + if the process is daemonized, but + :func:`bus.exit()` + if the process is attached to a TTY. This is because Unix window + managers tend to send SIGHUP to terminal windows when the user closes them. + + Feel free to add signals which are not available on every platform. The + :class:`SignalHandler` will ignore errors raised from attempting to register + handlers for unknown signals. + """ + + handlers = {} + """A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit).""" + + signals = {} + """A map from signal numbers to names.""" + + for k, v in vars(_signal).items(): + if k.startswith('SIG') and not k.startswith('SIG_'): + signals[v] = k + del k, v + + def __init__(self, bus): + self.bus = bus + # Set default handlers + self.handlers = {'SIGTERM': self.bus.exit, + 'SIGHUP': self.handle_SIGHUP, + 'SIGUSR1': self.bus.graceful, + } + + if sys.platform[:4] == 'java': + del self.handlers['SIGUSR1'] + self.handlers['SIGUSR2'] = self.bus.graceful + self.bus.log("SIGUSR1 cannot be set on the JVM platform. " + "Using SIGUSR2 instead.") + self.handlers['SIGINT'] = self._jython_SIGINT_handler + + self._previous_handlers = {} + + def _jython_SIGINT_handler(self, signum=None, frame=None): + # See http://bugs.jython.org/issue1313 + self.bus.log('Keyboard Interrupt: shutting down bus') + self.bus.exit() + + def subscribe(self): + """Subscribe self.handlers to signals.""" + for sig, func in self.handlers.items(): + try: + self.set_handler(sig, func) + except ValueError: + pass + + def unsubscribe(self): + """Unsubscribe self.handlers from signals.""" + for signum, handler in self._previous_handlers.items(): + signame = self.signals[signum] + + if handler is None: + self.bus.log("Restoring %s handler to SIG_DFL." % signame) + handler = _signal.SIG_DFL + else: + self.bus.log("Restoring %s handler %r." % (signame, handler)) + + try: + our_handler = _signal.signal(signum, handler) + if our_handler is None: + self.bus.log("Restored old %s handler %r, but our " + "handler was not registered." % + (signame, handler), level=30) + except ValueError: + self.bus.log("Unable to restore %s handler %r." % + (signame, handler), level=40, traceback=True) + + def set_handler(self, signal, listener=None): + """Subscribe a handler for the given signal (number or name). + + If the optional 'listener' argument is provided, it will be + subscribed as a listener for the given signal's channel. + + If the given signal name or number is not available on the current + platform, ValueError is raised. + """ + if isinstance(signal, basestring): + signum = getattr(_signal, signal, None) + if signum is None: + raise ValueError("No such signal: %r" % signal) + signame = signal + else: + try: + signame = self.signals[signal] + except KeyError: + raise ValueError("No such signal: %r" % signal) + signum = signal + + prev = _signal.signal(signum, self._handle_signal) + self._previous_handlers[signum] = prev + + if listener is not None: + self.bus.log("Listening for %s." % signame) + self.bus.subscribe(signame, listener) + + def _handle_signal(self, signum=None, frame=None): + """Python signal handler (self.set_handler subscribes it for you).""" + signame = self.signals[signum] + self.bus.log("Caught signal %s." % signame) + self.bus.publish(signame) + + def handle_SIGHUP(self): + """Restart if daemonized, else exit.""" + if os.isatty(sys.stdin.fileno()): + # not daemonized (may be foreground or background) + self.bus.log("SIGHUP caught but not daemonized. Exiting.") + self.bus.exit() + else: + self.bus.log("SIGHUP caught while daemonized. Restarting.") + self.bus.restart() + + +try: + import pwd, grp +except ImportError: + pwd, grp = None, None + + +class DropPrivileges(SimplePlugin): + """Drop privileges. uid/gid arguments not available on Windows. + + Special thanks to Gavin Baker: http://antonym.org/node/100. + """ + + def __init__(self, bus, umask=None, uid=None, gid=None): + SimplePlugin.__init__(self, bus) + self.finalized = False + self.uid = uid + self.gid = gid + self.umask = umask + + def _get_uid(self): + return self._uid + def _set_uid(self, val): + if val is not None: + if pwd is None: + self.bus.log("pwd module not available; ignoring uid.", + level=30) + val = None + elif isinstance(val, basestring): + val = pwd.getpwnam(val)[2] + self._uid = val + uid = property(_get_uid, _set_uid, + doc="The uid under which to run. Availability: Unix.") + + def _get_gid(self): + return self._gid + def _set_gid(self, val): + if val is not None: + if grp is None: + self.bus.log("grp module not available; ignoring gid.", + level=30) + val = None + elif isinstance(val, basestring): + val = grp.getgrnam(val)[2] + self._gid = val + gid = property(_get_gid, _set_gid, + doc="The gid under which to run. Availability: Unix.") + + def _get_umask(self): + return self._umask + def _set_umask(self, val): + if val is not None: + try: + os.umask + except AttributeError: + self.bus.log("umask function not available; ignoring umask.", + level=30) + val = None + self._umask = val + umask = property(_get_umask, _set_umask, + doc="""The default permission mode for newly created files and directories. + + Usually expressed in octal format, for example, ``0644``. + Availability: Unix, Windows. + """) + + def start(self): + # uid/gid + def current_ids(): + """Return the current (uid, gid) if available.""" + name, group = None, None + if pwd: + name = pwd.getpwuid(os.getuid())[0] + if grp: + group = grp.getgrgid(os.getgid())[0] + return name, group + + if self.finalized: + if not (self.uid is None and self.gid is None): + self.bus.log('Already running as uid: %r gid: %r' % + current_ids()) + else: + if self.uid is None and self.gid is None: + if pwd or grp: + self.bus.log('uid/gid not set', level=30) + else: + self.bus.log('Started as uid: %r gid: %r' % current_ids()) + if self.gid is not None: + os.setgid(self.gid) + os.setgroups([]) + if self.uid is not None: + os.setuid(self.uid) + self.bus.log('Running as uid: %r gid: %r' % current_ids()) + + # umask + if self.finalized: + if self.umask is not None: + self.bus.log('umask already set to: %03o' % self.umask) + else: + if self.umask is None: + self.bus.log('umask not set', level=30) + else: + old_umask = os.umask(self.umask) + self.bus.log('umask old: %03o, new: %03o' % + (old_umask, self.umask)) + + self.finalized = True + # This is slightly higher than the priority for server.start + # in order to facilitate the most common use: starting on a low + # port (which requires root) and then dropping to another user. + start.priority = 77 + + +class Daemonizer(SimplePlugin): + """Daemonize the running script. + + Use this with a Web Site Process Bus via:: + + Daemonizer(bus).subscribe() + + When this component finishes, the process is completely decoupled from + the parent environment. Please note that when this component is used, + the return code from the parent process will still be 0 if a startup + error occurs in the forked children. Errors in the initial daemonizing + process still return proper exit codes. Therefore, if you use this + plugin to daemonize, don't use the return code as an accurate indicator + of whether the process fully started. In fact, that return code only + indicates if the process succesfully finished the first fork. + """ + + def __init__(self, bus, stdin='/dev/null', stdout='/dev/null', + stderr='/dev/null'): + SimplePlugin.__init__(self, bus) + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.finalized = False + + def start(self): + if self.finalized: + self.bus.log('Already deamonized.') + + # forking has issues with threads: + # http://www.opengroup.org/onlinepubs/000095399/functions/fork.html + # "The general problem with making fork() work in a multi-threaded + # world is what to do with all of the threads..." + # So we check for active threads: + if threading.activeCount() != 1: + self.bus.log('There are %r active threads. ' + 'Daemonizing now may cause strange failures.' % + threading.enumerate(), level=30) + + # See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 + # (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7) + # and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012 + + # Finish up with the current stdout/stderr + sys.stdout.flush() + sys.stderr.flush() + + # Do first fork. + try: + pid = os.fork() + if pid == 0: + # This is the child process. Continue. + pass + else: + # This is the first parent. Exit, now that we've forked. + self.bus.log('Forking once.') + os._exit(0) + except OSError: + # Python raises OSError rather than returning negative numbers. + exc = sys.exc_info()[1] + sys.exit("%s: fork #1 failed: (%d) %s\n" + % (sys.argv[0], exc.errno, exc.strerror)) + + os.setsid() + + # Do second fork + try: + pid = os.fork() + if pid > 0: + self.bus.log('Forking twice.') + os._exit(0) # Exit second parent + except OSError: + exc = sys.exc_info()[1] + sys.exit("%s: fork #2 failed: (%d) %s\n" + % (sys.argv[0], exc.errno, exc.strerror)) + + os.chdir("/") + os.umask(0) + + si = open(self.stdin, "r") + so = open(self.stdout, "a+") + se = open(self.stderr, "a+") + + # os.dup2(fd, fd2) will close fd2 if necessary, + # so we don't explicitly close stdin/out/err. + # See http://docs.python.org/lib/os-fd-ops.html + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + self.bus.log('Daemonized to PID: %s' % os.getpid()) + self.finalized = True + start.priority = 65 + + +class PIDFile(SimplePlugin): + """Maintain a PID file via a WSPBus.""" + + def __init__(self, bus, pidfile): + SimplePlugin.__init__(self, bus) + self.pidfile = pidfile + self.finalized = False + + def start(self): + pid = os.getpid() + if self.finalized: + self.bus.log('PID %r already written to %r.' % (pid, self.pidfile)) + else: + open(self.pidfile, "wb").write(ntob("%s" % pid, 'utf8')) + self.bus.log('PID %r written to %r.' % (pid, self.pidfile)) + self.finalized = True + start.priority = 70 + + def exit(self): + try: + os.remove(self.pidfile) + self.bus.log('PID file removed: %r.' % self.pidfile) + except (KeyboardInterrupt, SystemExit): + raise + except: + pass + + +class PerpetualTimer(threading._Timer): + """A responsive subclass of threading._Timer whose run() method repeats. + + Use this timer only when you really need a very interruptible timer; + this checks its 'finished' condition up to 20 times a second, which can + results in pretty high CPU usage + """ + + def run(self): + while True: + self.finished.wait(self.interval) + if self.finished.isSet(): + return + try: + self.function(*self.args, **self.kwargs) + except Exception: + self.bus.log("Error in perpetual timer thread function %r." % + self.function, level=40, traceback=True) + # Quit on first error to avoid massive logs. + raise + + +class BackgroundTask(threading.Thread): + """A subclass of threading.Thread whose run() method repeats. + + Use this class for most repeating tasks. It uses time.sleep() to wait + for each interval, which isn't very responsive; that is, even if you call + self.cancel(), you'll have to wait until the sleep() call finishes before + the thread stops. To compensate, it defaults to being daemonic, which means + it won't delay stopping the whole process. + """ + + def __init__(self, interval, function, args=[], kwargs={}, bus=None): + threading.Thread.__init__(self) + self.interval = interval + self.function = function + self.args = args + self.kwargs = kwargs + self.running = False + self.bus = bus + + def cancel(self): + self.running = False + + def run(self): + self.running = True + while self.running: + time.sleep(self.interval) + if not self.running: + return + try: + self.function(*self.args, **self.kwargs) + except Exception: + if self.bus: + self.bus.log("Error in background task thread function %r." + % self.function, level=40, traceback=True) + # Quit on first error to avoid massive logs. + raise + + def _set_daemon(self): + return True + + +class Monitor(SimplePlugin): + """WSPBus listener to periodically run a callback in its own thread.""" + + callback = None + """The function to call at intervals.""" + + frequency = 60 + """The time in seconds between callback runs.""" + + thread = None + """A :class:`BackgroundTask` thread.""" + + def __init__(self, bus, callback, frequency=60, name=None): + SimplePlugin.__init__(self, bus) + self.callback = callback + self.frequency = frequency + self.thread = None + self.name = name + + def start(self): + """Start our callback in its own background thread.""" + if self.frequency > 0: + threadname = self.name or self.__class__.__name__ + if self.thread is None: + self.thread = BackgroundTask(self.frequency, self.callback, + bus = self.bus) + self.thread.setName(threadname) + self.thread.start() + self.bus.log("Started monitor thread %r." % threadname) + else: + self.bus.log("Monitor thread %r already started." % threadname) + start.priority = 70 + + def stop(self): + """Stop our callback's background task thread.""" + if self.thread is None: + self.bus.log("No thread running for %s." % self.name or self.__class__.__name__) + else: + if self.thread is not threading.currentThread(): + name = self.thread.getName() + self.thread.cancel() + if not get_daemon(self.thread): + self.bus.log("Joining %r" % name) + self.thread.join() + self.bus.log("Stopped thread %r." % name) + self.thread = None + + def graceful(self): + """Stop the callback's background task thread and restart it.""" + self.stop() + self.start() + + +class Autoreloader(Monitor): + """Monitor which re-executes the process when files change. + + This :ref:`plugin` restarts the process (via :func:`os.execv`) + if any of the files it monitors change (or is deleted). By default, the + autoreloader monitors all imported modules; you can add to the + set by adding to ``autoreload.files``:: + + cherrypy.engine.autoreload.files.add(myFile) + + If there are imported files you do *not* wish to monitor, you can adjust the + ``match`` attribute, a regular expression. For example, to stop monitoring + cherrypy itself:: + + cherrypy.engine.autoreload.match = r'^(?!cherrypy).+' + + Like all :class:`Monitor` plugins, + the autoreload plugin takes a ``frequency`` argument. The default is + 1 second; that is, the autoreloader will examine files once each second. + """ + + files = None + """The set of files to poll for modifications.""" + + frequency = 1 + """The interval in seconds at which to poll for modified files.""" + + match = '.*' + """A regular expression by which to match filenames.""" + + def __init__(self, bus, frequency=1, match='.*'): + self.mtimes = {} + self.files = set() + self.match = match + Monitor.__init__(self, bus, self.run, frequency) + + def start(self): + """Start our own background task thread for self.run.""" + if self.thread is None: + self.mtimes = {} + Monitor.start(self) + start.priority = 70 + + def sysfiles(self): + """Return a Set of sys.modules filenames to monitor.""" + files = set() + for k, m in sys.modules.items(): + if re.match(self.match, k): + if hasattr(m, '__loader__') and hasattr(m.__loader__, 'archive'): + f = m.__loader__.archive + else: + f = getattr(m, '__file__', None) + if f is not None and not os.path.isabs(f): + # ensure absolute paths so a os.chdir() in the app doesn't break me + f = os.path.normpath(os.path.join(_module__file__base, f)) + files.add(f) + return files + + def run(self): + """Reload the process if registered files have been modified.""" + for filename in self.sysfiles() | self.files: + if filename: + if filename.endswith('.pyc'): + filename = filename[:-1] + + oldtime = self.mtimes.get(filename, 0) + if oldtime is None: + # Module with no .py file. Skip it. + continue + + try: + mtime = os.stat(filename).st_mtime + except OSError: + # Either a module with no .py file, or it's been deleted. + mtime = None + + if filename not in self.mtimes: + # If a module has no .py file, this will be None. + self.mtimes[filename] = mtime + else: + if mtime is None or mtime > oldtime: + # The file has been deleted or modified. + self.bus.log("Restarting because %s changed." % filename) + self.thread.cancel() + self.bus.log("Stopped thread %r." % self.thread.getName()) + self.bus.restart() + return + + +class ThreadManager(SimplePlugin): + """Manager for HTTP request threads. + + If you have control over thread creation and destruction, publish to + the 'acquire_thread' and 'release_thread' channels (for each thread). + This will register/unregister the current thread and publish to + 'start_thread' and 'stop_thread' listeners in the bus as needed. + + If threads are created and destroyed by code you do not control + (e.g., Apache), then, at the beginning of every HTTP request, + publish to 'acquire_thread' only. You should not publish to + 'release_thread' in this case, since you do not know whether + the thread will be re-used or not. The bus will call + 'stop_thread' listeners for you when it stops. + """ + + threads = None + """A map of {thread ident: index number} pairs.""" + + def __init__(self, bus): + self.threads = {} + SimplePlugin.__init__(self, bus) + self.bus.listeners.setdefault('acquire_thread', set()) + self.bus.listeners.setdefault('start_thread', set()) + self.bus.listeners.setdefault('release_thread', set()) + self.bus.listeners.setdefault('stop_thread', set()) + + def acquire_thread(self): + """Run 'start_thread' listeners for the current thread. + + If the current thread has already been seen, any 'start_thread' + listeners will not be run again. + """ + thread_ident = get_thread_ident() + if thread_ident not in self.threads: + # We can't just use get_ident as the thread ID + # because some platforms reuse thread ID's. + i = len(self.threads) + 1 + self.threads[thread_ident] = i + self.bus.publish('start_thread', i) + + def release_thread(self): + """Release the current thread and run 'stop_thread' listeners.""" + thread_ident = get_thread_ident() + i = self.threads.pop(thread_ident, None) + if i is not None: + self.bus.publish('stop_thread', i) + + def stop(self): + """Release all threads and run all 'stop_thread' listeners.""" + for thread_ident, i in self.threads.items(): + self.bus.publish('stop_thread', i) + self.threads.clear() + graceful = stop + diff --git a/libs/cherrypy/process/servers.py b/libs/cherrypy/process/servers.py new file mode 100644 index 0000000..fa714d6 --- /dev/null +++ b/libs/cherrypy/process/servers.py @@ -0,0 +1,427 @@ +""" +Starting in CherryPy 3.1, cherrypy.server is implemented as an +:ref:`Engine Plugin`. It's an instance of +:class:`cherrypy._cpserver.Server`, which is a subclass of +:class:`cherrypy.process.servers.ServerAdapter`. The ``ServerAdapter`` class +is designed to control other servers, as well. + +Multiple servers/ports +====================== + +If you need to start more than one HTTP server (to serve on multiple ports, or +protocols, etc.), you can manually register each one and then start them all +with engine.start:: + + s1 = ServerAdapter(cherrypy.engine, MyWSGIServer(host='0.0.0.0', port=80)) + s2 = ServerAdapter(cherrypy.engine, another.HTTPServer(host='127.0.0.1', SSL=True)) + s1.subscribe() + s2.subscribe() + cherrypy.engine.start() + +.. index:: SCGI + +FastCGI/SCGI +============ + +There are also Flup\ **F**\ CGIServer and Flup\ **S**\ CGIServer classes in +:mod:`cherrypy.process.servers`. To start an fcgi server, for example, +wrap an instance of it in a ServerAdapter:: + + addr = ('0.0.0.0', 4000) + f = servers.FlupFCGIServer(application=cherrypy.tree, bindAddress=addr) + s = servers.ServerAdapter(cherrypy.engine, httpserver=f, bind_addr=addr) + s.subscribe() + +The :doc:`cherryd` startup script will do the above for +you via its `-f` flag. +Note that you need to download and install `flup `_ +yourself, whether you use ``cherryd`` or not. + +.. _fastcgi: +.. index:: FastCGI + +FastCGI +------- + +A very simple setup lets your cherry run with FastCGI. +You just need the flup library, +plus a running Apache server (with ``mod_fastcgi``) or lighttpd server. + +CherryPy code +^^^^^^^^^^^^^ + +hello.py:: + + #!/usr/bin/python + import cherrypy + + class HelloWorld: + \"""Sample request handler class.\""" + def index(self): + return "Hello world!" + index.exposed = True + + cherrypy.tree.mount(HelloWorld()) + # CherryPy autoreload must be disabled for the flup server to work + cherrypy.config.update({'engine.autoreload_on':False}) + +Then run :doc:`/deployguide/cherryd` with the '-f' arg:: + + cherryd -c -d -f -i hello.py + +Apache +^^^^^^ + +At the top level in httpd.conf:: + + FastCgiIpcDir /tmp + FastCgiServer /path/to/cherry.fcgi -idle-timeout 120 -processes 4 + +And inside the relevant VirtualHost section:: + + # FastCGI config + AddHandler fastcgi-script .fcgi + ScriptAliasMatch (.*$) /path/to/cherry.fcgi$1 + +Lighttpd +^^^^^^^^ + +For `Lighttpd `_ you can follow these +instructions. Within ``lighttpd.conf`` make sure ``mod_fastcgi`` is +active within ``server.modules``. Then, within your ``$HTTP["host"]`` +directive, configure your fastcgi script like the following:: + + $HTTP["url"] =~ "" { + fastcgi.server = ( + "/" => ( + "script.fcgi" => ( + "bin-path" => "/path/to/your/script.fcgi", + "socket" => "/tmp/script.sock", + "check-local" => "disable", + "disable-time" => 1, + "min-procs" => 1, + "max-procs" => 1, # adjust as needed + ), + ), + ) + } # end of $HTTP["url"] =~ "^/" + +Please see `Lighttpd FastCGI Docs +`_ for an explanation +of the possible configuration options. +""" + +import sys +import time + + +class ServerAdapter(object): + """Adapter for an HTTP server. + + If you need to start more than one HTTP server (to serve on multiple + ports, or protocols, etc.), you can manually register each one and then + start them all with bus.start: + + s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80)) + s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True)) + s1.subscribe() + s2.subscribe() + bus.start() + """ + + def __init__(self, bus, httpserver=None, bind_addr=None): + self.bus = bus + self.httpserver = httpserver + self.bind_addr = bind_addr + self.interrupt = None + self.running = False + + def subscribe(self): + self.bus.subscribe('start', self.start) + self.bus.subscribe('stop', self.stop) + + def unsubscribe(self): + self.bus.unsubscribe('start', self.start) + self.bus.unsubscribe('stop', self.stop) + + def start(self): + """Start the HTTP server.""" + if self.bind_addr is None: + on_what = "unknown interface (dynamic?)" + elif isinstance(self.bind_addr, tuple): + host, port = self.bind_addr + on_what = "%s:%s" % (host, port) + else: + on_what = "socket file: %s" % self.bind_addr + + if self.running: + self.bus.log("Already serving on %s" % on_what) + return + + self.interrupt = None + if not self.httpserver: + raise ValueError("No HTTP server has been created.") + + # Start the httpserver in a new thread. + if isinstance(self.bind_addr, tuple): + wait_for_free_port(*self.bind_addr) + + import threading + t = threading.Thread(target=self._start_http_thread) + t.setName("HTTPServer " + t.getName()) + t.start() + + self.wait() + self.running = True + self.bus.log("Serving on %s" % on_what) + start.priority = 75 + + def _start_http_thread(self): + """HTTP servers MUST be running in new threads, so that the + main thread persists to receive KeyboardInterrupt's. If an + exception is raised in the httpserver's thread then it's + trapped here, and the bus (and therefore our httpserver) + are shut down. + """ + try: + self.httpserver.start() + except KeyboardInterrupt: + self.bus.log(" hit: shutting down HTTP server") + self.interrupt = sys.exc_info()[1] + self.bus.exit() + except SystemExit: + self.bus.log("SystemExit raised: shutting down HTTP server") + self.interrupt = sys.exc_info()[1] + self.bus.exit() + raise + except: + self.interrupt = sys.exc_info()[1] + self.bus.log("Error in HTTP server: shutting down", + traceback=True, level=40) + self.bus.exit() + raise + + def wait(self): + """Wait until the HTTP server is ready to receive requests.""" + while not getattr(self.httpserver, "ready", False): + if self.interrupt: + raise self.interrupt + time.sleep(.1) + + # Wait for port to be occupied + if isinstance(self.bind_addr, tuple): + host, port = self.bind_addr + wait_for_occupied_port(host, port) + + def stop(self): + """Stop the HTTP server.""" + if self.running: + # stop() MUST block until the server is *truly* stopped. + self.httpserver.stop() + # Wait for the socket to be truly freed. + if isinstance(self.bind_addr, tuple): + wait_for_free_port(*self.bind_addr) + self.running = False + self.bus.log("HTTP Server %s shut down" % self.httpserver) + else: + self.bus.log("HTTP Server %s already shut down" % self.httpserver) + stop.priority = 25 + + def restart(self): + """Restart the HTTP server.""" + self.stop() + self.start() + + +class FlupCGIServer(object): + """Adapter for a flup.server.cgi.WSGIServer.""" + + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + self.ready = False + + def start(self): + """Start the CGI server.""" + # We have to instantiate the server class here because its __init__ + # starts a threadpool. If we do it too early, daemonize won't work. + from flup.server.cgi import WSGIServer + + self.cgiserver = WSGIServer(*self.args, **self.kwargs) + self.ready = True + self.cgiserver.run() + + def stop(self): + """Stop the HTTP server.""" + self.ready = False + + +class FlupFCGIServer(object): + """Adapter for a flup.server.fcgi.WSGIServer.""" + + def __init__(self, *args, **kwargs): + if kwargs.get('bindAddress', None) is None: + import socket + if not hasattr(socket, 'fromfd'): + raise ValueError( + 'Dynamic FCGI server not available on this platform. ' + 'You must use a static or external one by providing a ' + 'legal bindAddress.') + self.args = args + self.kwargs = kwargs + self.ready = False + + def start(self): + """Start the FCGI server.""" + # We have to instantiate the server class here because its __init__ + # starts a threadpool. If we do it too early, daemonize won't work. + from flup.server.fcgi import WSGIServer + self.fcgiserver = WSGIServer(*self.args, **self.kwargs) + # TODO: report this bug upstream to flup. + # If we don't set _oldSIGs on Windows, we get: + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 108, in run + # self._restoreSignalHandlers() + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 156, in _restoreSignalHandlers + # for signum,handler in self._oldSIGs: + # AttributeError: 'WSGIServer' object has no attribute '_oldSIGs' + self.fcgiserver._installSignalHandlers = lambda: None + self.fcgiserver._oldSIGs = [] + self.ready = True + self.fcgiserver.run() + + def stop(self): + """Stop the HTTP server.""" + # Forcibly stop the fcgi server main event loop. + self.fcgiserver._keepGoing = False + # Force all worker threads to die off. + self.fcgiserver._threadPool.maxSpare = self.fcgiserver._threadPool._idleCount + self.ready = False + + +class FlupSCGIServer(object): + """Adapter for a flup.server.scgi.WSGIServer.""" + + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + self.ready = False + + def start(self): + """Start the SCGI server.""" + # We have to instantiate the server class here because its __init__ + # starts a threadpool. If we do it too early, daemonize won't work. + from flup.server.scgi import WSGIServer + self.scgiserver = WSGIServer(*self.args, **self.kwargs) + # TODO: report this bug upstream to flup. + # If we don't set _oldSIGs on Windows, we get: + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 108, in run + # self._restoreSignalHandlers() + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 156, in _restoreSignalHandlers + # for signum,handler in self._oldSIGs: + # AttributeError: 'WSGIServer' object has no attribute '_oldSIGs' + self.scgiserver._installSignalHandlers = lambda: None + self.scgiserver._oldSIGs = [] + self.ready = True + self.scgiserver.run() + + def stop(self): + """Stop the HTTP server.""" + self.ready = False + # Forcibly stop the scgi server main event loop. + self.scgiserver._keepGoing = False + # Force all worker threads to die off. + self.scgiserver._threadPool.maxSpare = 0 + + +def client_host(server_host): + """Return the host on which a client can connect to the given listener.""" + if server_host == '0.0.0.0': + # 0.0.0.0 is INADDR_ANY, which should answer on localhost. + return '127.0.0.1' + if server_host in ('::', '::0', '::0.0.0.0'): + # :: is IN6ADDR_ANY, which should answer on localhost. + # ::0 and ::0.0.0.0 are non-canonical but common ways to write IN6ADDR_ANY. + return '::1' + return server_host + +def check_port(host, port, timeout=1.0): + """Raise an error if the given port is not free on the given host.""" + if not host: + raise ValueError("Host values of '' or None are not allowed.") + host = client_host(host) + port = int(port) + + import socket + + # AF_INET or AF_INET6 socket + # Get the correct address family for our host (allows IPv6 addresses) + try: + info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM) + except socket.gaierror: + if ':' in host: + info = [(socket.AF_INET6, socket.SOCK_STREAM, 0, "", (host, port, 0, 0))] + else: + info = [(socket.AF_INET, socket.SOCK_STREAM, 0, "", (host, port))] + + for res in info: + af, socktype, proto, canonname, sa = res + s = None + try: + s = socket.socket(af, socktype, proto) + # See http://groups.google.com/group/cherrypy-users/ + # browse_frm/thread/bbfe5eb39c904fe0 + s.settimeout(timeout) + s.connect((host, port)) + s.close() + raise IOError("Port %s is in use on %s; perhaps the previous " + "httpserver did not shut down properly." % + (repr(port), repr(host))) + except socket.error: + if s: + s.close() + + +# Feel free to increase these defaults on slow systems: +free_port_timeout = 0.1 +occupied_port_timeout = 1.0 + +def wait_for_free_port(host, port, timeout=None): + """Wait for the specified port to become free (drop requests).""" + if not host: + raise ValueError("Host values of '' or None are not allowed.") + if timeout is None: + timeout = free_port_timeout + + for trial in range(50): + try: + # we are expecting a free port, so reduce the timeout + check_port(host, port, timeout=timeout) + except IOError: + # Give the old server thread time to free the port. + time.sleep(timeout) + else: + return + + raise IOError("Port %r not free on %r" % (port, host)) + +def wait_for_occupied_port(host, port, timeout=None): + """Wait for the specified port to become active (receive requests).""" + if not host: + raise ValueError("Host values of '' or None are not allowed.") + if timeout is None: + timeout = occupied_port_timeout + + for trial in range(50): + try: + check_port(host, port, timeout=timeout) + except IOError: + return + else: + time.sleep(timeout) + + raise IOError("Port %r not bound on %r" % (port, host)) diff --git a/libs/cherrypy/process/win32.py b/libs/cherrypy/process/win32.py new file mode 100644 index 0000000..83f99a5 --- /dev/null +++ b/libs/cherrypy/process/win32.py @@ -0,0 +1,174 @@ +"""Windows service. Requires pywin32.""" + +import os +import win32api +import win32con +import win32event +import win32service +import win32serviceutil + +from cherrypy.process import wspbus, plugins + + +class ConsoleCtrlHandler(plugins.SimplePlugin): + """A WSPBus plugin for handling Win32 console events (like Ctrl-C).""" + + def __init__(self, bus): + self.is_set = False + plugins.SimplePlugin.__init__(self, bus) + + def start(self): + if self.is_set: + self.bus.log('Handler for console events already set.', level=40) + return + + result = win32api.SetConsoleCtrlHandler(self.handle, 1) + if result == 0: + self.bus.log('Could not SetConsoleCtrlHandler (error %r)' % + win32api.GetLastError(), level=40) + else: + self.bus.log('Set handler for console events.', level=40) + self.is_set = True + + def stop(self): + if not self.is_set: + self.bus.log('Handler for console events already off.', level=40) + return + + try: + result = win32api.SetConsoleCtrlHandler(self.handle, 0) + except ValueError: + # "ValueError: The object has not been registered" + result = 1 + + if result == 0: + self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' % + win32api.GetLastError(), level=40) + else: + self.bus.log('Removed handler for console events.', level=40) + self.is_set = False + + def handle(self, event): + """Handle console control events (like Ctrl-C).""" + if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT, + win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT, + win32con.CTRL_CLOSE_EVENT): + self.bus.log('Console event %s: shutting down bus' % event) + + # Remove self immediately so repeated Ctrl-C doesn't re-call it. + try: + self.stop() + except ValueError: + pass + + self.bus.exit() + # 'First to return True stops the calls' + return 1 + return 0 + + +class Win32Bus(wspbus.Bus): + """A Web Site Process Bus implementation for Win32. + + Instead of time.sleep, this bus blocks using native win32event objects. + """ + + def __init__(self): + self.events = {} + wspbus.Bus.__init__(self) + + def _get_state_event(self, state): + """Return a win32event for the given state (creating it if needed).""" + try: + return self.events[state] + except KeyError: + event = win32event.CreateEvent(None, 0, 0, + "WSPBus %s Event (pid=%r)" % + (state.name, os.getpid())) + self.events[state] = event + return event + + def _get_state(self): + return self._state + def _set_state(self, value): + self._state = value + event = self._get_state_event(value) + win32event.PulseEvent(event) + state = property(_get_state, _set_state) + + def wait(self, state, interval=0.1, channel=None): + """Wait for the given state(s), KeyboardInterrupt or SystemExit. + + Since this class uses native win32event objects, the interval + argument is ignored. + """ + if isinstance(state, (tuple, list)): + # Don't wait for an event that beat us to the punch ;) + if self.state not in state: + events = tuple([self._get_state_event(s) for s in state]) + win32event.WaitForMultipleObjects(events, 0, win32event.INFINITE) + else: + # Don't wait for an event that beat us to the punch ;) + if self.state != state: + event = self._get_state_event(state) + win32event.WaitForSingleObject(event, win32event.INFINITE) + + +class _ControlCodes(dict): + """Control codes used to "signal" a service via ControlService. + + User-defined control codes are in the range 128-255. We generally use + the standard Python value for the Linux signal and add 128. Example: + + >>> signal.SIGUSR1 + 10 + control_codes['graceful'] = 128 + 10 + """ + + def key_for(self, obj): + """For the given value, return its corresponding key.""" + for key, val in self.items(): + if val is obj: + return key + raise ValueError("The given object could not be found: %r" % obj) + +control_codes = _ControlCodes({'graceful': 138}) + + +def signal_child(service, command): + if command == 'stop': + win32serviceutil.StopService(service) + elif command == 'restart': + win32serviceutil.RestartService(service) + else: + win32serviceutil.ControlService(service, control_codes[command]) + + +class PyWebService(win32serviceutil.ServiceFramework): + """Python Web Service.""" + + _svc_name_ = "Python Web Service" + _svc_display_name_ = "Python Web Service" + _svc_deps_ = None # sequence of service names on which this depends + _exe_name_ = "pywebsvc" + _exe_args_ = None # Default to no arguments + + # Only exists on Windows 2000 or later, ignored on windows NT + _svc_description_ = "Python Web Service" + + def SvcDoRun(self): + from cherrypy import process + process.bus.start() + process.bus.block() + + def SvcStop(self): + from cherrypy import process + self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) + process.bus.exit() + + def SvcOther(self, control): + process.bus.publish(control_codes.key_for(control)) + + +if __name__ == '__main__': + win32serviceutil.HandleCommandLine(PyWebService) diff --git a/libs/cherrypy/process/wspbus.py b/libs/cherrypy/process/wspbus.py new file mode 100644 index 0000000..6ef768d --- /dev/null +++ b/libs/cherrypy/process/wspbus.py @@ -0,0 +1,432 @@ +"""An implementation of the Web Site Process Bus. + +This module is completely standalone, depending only on the stdlib. + +Web Site Process Bus +-------------------- + +A Bus object is used to contain and manage site-wide behavior: +daemonization, HTTP server start/stop, process reload, signal handling, +drop privileges, PID file management, logging for all of these, +and many more. + +In addition, a Bus object provides a place for each web framework +to register code that runs in response to site-wide events (like +process start and stop), or which controls or otherwise interacts with +the site-wide components mentioned above. For example, a framework which +uses file-based templates would add known template filenames to an +autoreload component. + +Ideally, a Bus object will be flexible enough to be useful in a variety +of invocation scenarios: + + 1. The deployer starts a site from the command line via a + framework-neutral deployment script; applications from multiple frameworks + are mixed in a single site. Command-line arguments and configuration + files are used to define site-wide components such as the HTTP server, + WSGI component graph, autoreload behavior, signal handling, etc. + 2. The deployer starts a site via some other process, such as Apache; + applications from multiple frameworks are mixed in a single site. + Autoreload and signal handling (from Python at least) are disabled. + 3. The deployer starts a site via a framework-specific mechanism; + for example, when running tests, exploring tutorials, or deploying + single applications from a single framework. The framework controls + which site-wide components are enabled as it sees fit. + +The Bus object in this package uses topic-based publish-subscribe +messaging to accomplish all this. A few topic channels are built in +('start', 'stop', 'exit', 'graceful', 'log', and 'main'). Frameworks and +site containers are free to define their own. If a message is sent to a +channel that has not been defined or has no listeners, there is no effect. + +In general, there should only ever be a single Bus object per process. +Frameworks and site containers share a single Bus object by publishing +messages and subscribing listeners. + +The Bus object works as a finite state machine which models the current +state of the process. Bus methods move it from one state to another; +those methods then publish to subscribed listeners on the channel for +the new state.:: + + O + | + V + STOPPING --> STOPPED --> EXITING -> X + A A | + | \___ | + | \ | + | V V + STARTED <-- STARTING + +""" + +import atexit +import os +import sys +import threading +import time +import traceback as _traceback +import warnings + +from cherrypy._cpcompat import set + +# Here I save the value of os.getcwd(), which, if I am imported early enough, +# will be the directory from which the startup script was run. This is needed +# by _do_execv(), to change back to the original directory before execv()ing a +# new process. This is a defense against the application having changed the +# current working directory (which could make sys.executable "not found" if +# sys.executable is a relative-path, and/or cause other problems). +_startup_cwd = os.getcwd() + +class ChannelFailures(Exception): + """Exception raised when errors occur in a listener during Bus.publish().""" + delimiter = '\n' + + def __init__(self, *args, **kwargs): + # Don't use 'super' here; Exceptions are old-style in Py2.4 + # See http://www.cherrypy.org/ticket/959 + Exception.__init__(self, *args, **kwargs) + self._exceptions = list() + + def handle_exception(self): + """Append the current exception to self.""" + self._exceptions.append(sys.exc_info()[1]) + + def get_instances(self): + """Return a list of seen exception instances.""" + return self._exceptions[:] + + def __str__(self): + exception_strings = map(repr, self.get_instances()) + return self.delimiter.join(exception_strings) + + __repr__ = __str__ + + def __bool__(self): + return bool(self._exceptions) + __nonzero__ = __bool__ + +# Use a flag to indicate the state of the bus. +class _StateEnum(object): + class State(object): + name = None + def __repr__(self): + return "states.%s" % self.name + + def __setattr__(self, key, value): + if isinstance(value, self.State): + value.name = key + object.__setattr__(self, key, value) +states = _StateEnum() +states.STOPPED = states.State() +states.STARTING = states.State() +states.STARTED = states.State() +states.STOPPING = states.State() +states.EXITING = states.State() + + +try: + import fcntl +except ImportError: + max_files = 0 +else: + try: + max_files = os.sysconf('SC_OPEN_MAX') + except AttributeError: + max_files = 1024 + + +class Bus(object): + """Process state-machine and messenger for HTTP site deployment. + + All listeners for a given channel are guaranteed to be called even + if others at the same channel fail. Each failure is logged, but + execution proceeds on to the next listener. The only way to stop all + processing from inside a listener is to raise SystemExit and stop the + whole server. + """ + + states = states + state = states.STOPPED + execv = False + max_cloexec_files = max_files + + def __init__(self): + self.execv = False + self.state = states.STOPPED + self.listeners = dict( + [(channel, set()) for channel + in ('start', 'stop', 'exit', 'graceful', 'log', 'main')]) + self._priorities = {} + + def subscribe(self, channel, callback, priority=None): + """Add the given callback at the given channel (if not present).""" + if channel not in self.listeners: + self.listeners[channel] = set() + self.listeners[channel].add(callback) + + if priority is None: + priority = getattr(callback, 'priority', 50) + self._priorities[(channel, callback)] = priority + + def unsubscribe(self, channel, callback): + """Discard the given callback (if present).""" + listeners = self.listeners.get(channel) + if listeners and callback in listeners: + listeners.discard(callback) + del self._priorities[(channel, callback)] + + def publish(self, channel, *args, **kwargs): + """Return output of all subscribers for the given channel.""" + if channel not in self.listeners: + return [] + + exc = ChannelFailures() + output = [] + + items = [(self._priorities[(channel, listener)], listener) + for listener in self.listeners[channel]] + try: + items.sort(key=lambda item: item[0]) + except TypeError: + # Python 2.3 had no 'key' arg, but that doesn't matter + # since it could sort dissimilar types just fine. + items.sort() + for priority, listener in items: + try: + output.append(listener(*args, **kwargs)) + except KeyboardInterrupt: + raise + except SystemExit: + e = sys.exc_info()[1] + # If we have previous errors ensure the exit code is non-zero + if exc and e.code == 0: + e.code = 1 + raise + except: + exc.handle_exception() + if channel == 'log': + # Assume any further messages to 'log' will fail. + pass + else: + self.log("Error in %r listener %r" % (channel, listener), + level=40, traceback=True) + if exc: + raise exc + return output + + def _clean_exit(self): + """An atexit handler which asserts the Bus is not running.""" + if self.state != states.EXITING: + warnings.warn( + "The main thread is exiting, but the Bus is in the %r state; " + "shutting it down automatically now. You must either call " + "bus.block() after start(), or call bus.exit() before the " + "main thread exits." % self.state, RuntimeWarning) + self.exit() + + def start(self): + """Start all services.""" + atexit.register(self._clean_exit) + + self.state = states.STARTING + self.log('Bus STARTING') + try: + self.publish('start') + self.state = states.STARTED + self.log('Bus STARTED') + except (KeyboardInterrupt, SystemExit): + raise + except: + self.log("Shutting down due to error in start listener:", + level=40, traceback=True) + e_info = sys.exc_info()[1] + try: + self.exit() + except: + # Any stop/exit errors will be logged inside publish(). + pass + # Re-raise the original error + raise e_info + + def exit(self): + """Stop all services and prepare to exit the process.""" + exitstate = self.state + try: + self.stop() + + self.state = states.EXITING + self.log('Bus EXITING') + self.publish('exit') + # This isn't strictly necessary, but it's better than seeing + # "Waiting for child threads to terminate..." and then nothing. + self.log('Bus EXITED') + except: + # This method is often called asynchronously (whether thread, + # signal handler, console handler, or atexit handler), so we + # can't just let exceptions propagate out unhandled. + # Assume it's been logged and just die. + os._exit(70) # EX_SOFTWARE + + if exitstate == states.STARTING: + # exit() was called before start() finished, possibly due to + # Ctrl-C because a start listener got stuck. In this case, + # we could get stuck in a loop where Ctrl-C never exits the + # process, so we just call os.exit here. + os._exit(70) # EX_SOFTWARE + + def restart(self): + """Restart the process (may close connections). + + This method does not restart the process from the calling thread; + instead, it stops the bus and asks the main thread to call execv. + """ + self.execv = True + self.exit() + + def graceful(self): + """Advise all services to reload.""" + self.log('Bus graceful') + self.publish('graceful') + + def block(self, interval=0.1): + """Wait for the EXITING state, KeyboardInterrupt or SystemExit. + + This function is intended to be called only by the main thread. + After waiting for the EXITING state, it also waits for all threads + to terminate, and then calls os.execv if self.execv is True. This + design allows another thread to call bus.restart, yet have the main + thread perform the actual execv call (required on some platforms). + """ + try: + self.wait(states.EXITING, interval=interval, channel='main') + except (KeyboardInterrupt, IOError): + # The time.sleep call might raise + # "IOError: [Errno 4] Interrupted function call" on KBInt. + self.log('Keyboard Interrupt: shutting down bus') + self.exit() + except SystemExit: + self.log('SystemExit raised: shutting down bus') + self.exit() + raise + + # Waiting for ALL child threads to finish is necessary on OS X. + # See http://www.cherrypy.org/ticket/581. + # It's also good to let them all shut down before allowing + # the main thread to call atexit handlers. + # See http://www.cherrypy.org/ticket/751. + self.log("Waiting for child threads to terminate...") + for t in threading.enumerate(): + if t != threading.currentThread() and t.isAlive(): + # Note that any dummy (external) threads are always daemonic. + if hasattr(threading.Thread, "daemon"): + # Python 2.6+ + d = t.daemon + else: + d = t.isDaemon() + if not d: + self.log("Waiting for thread %s." % t.getName()) + t.join() + + if self.execv: + self._do_execv() + + def wait(self, state, interval=0.1, channel=None): + """Poll for the given state(s) at intervals; publish to channel.""" + if isinstance(state, (tuple, list)): + states = state + else: + states = [state] + + def _wait(): + while self.state not in states: + time.sleep(interval) + self.publish(channel) + + # From http://psyco.sourceforge.net/psycoguide/bugs.html: + # "The compiled machine code does not include the regular polling + # done by Python, meaning that a KeyboardInterrupt will not be + # detected before execution comes back to the regular Python + # interpreter. Your program cannot be interrupted if caught + # into an infinite Psyco-compiled loop." + try: + sys.modules['psyco'].cannotcompile(_wait) + except (KeyError, AttributeError): + pass + + _wait() + + def _do_execv(self): + """Re-execute the current process. + + This must be called from the main thread, because certain platforms + (OS X) don't allow execv to be called in a child thread very well. + """ + args = sys.argv[:] + self.log('Re-spawning %s' % ' '.join(args)) + + if sys.platform[:4] == 'java': + from _systemrestart import SystemRestart + raise SystemRestart + else: + args.insert(0, sys.executable) + if sys.platform == 'win32': + args = ['"%s"' % arg for arg in args] + + os.chdir(_startup_cwd) + if self.max_cloexec_files: + self._set_cloexec() + os.execv(sys.executable, args) + + def _set_cloexec(self): + """Set the CLOEXEC flag on all open files (except stdin/out/err). + + If self.max_cloexec_files is an integer (the default), then on + platforms which support it, it represents the max open files setting + for the operating system. This function will be called just before + the process is restarted via os.execv() to prevent open files + from persisting into the new process. + + Set self.max_cloexec_files to 0 to disable this behavior. + """ + for fd in range(3, self.max_cloexec_files): # skip stdin/out/err + try: + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + except IOError: + continue + fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) + + def stop(self): + """Stop all services.""" + self.state = states.STOPPING + self.log('Bus STOPPING') + self.publish('stop') + self.state = states.STOPPED + self.log('Bus STOPPED') + + def start_with_callback(self, func, args=None, kwargs=None): + """Start 'func' in a new thread T, then start self (and return T).""" + if args is None: + args = () + if kwargs is None: + kwargs = {} + args = (func,) + args + + def _callback(func, *a, **kw): + self.wait(states.STARTED) + func(*a, **kw) + t = threading.Thread(target=_callback, args=args, kwargs=kwargs) + t.setName('Bus Callback ' + t.getName()) + t.start() + + self.start() + + return t + + def log(self, msg="", level=20, traceback=False): + """Log the given message. Append the last traceback if requested.""" + if traceback: + msg += "\n" + "".join(_traceback.format_exception(*sys.exc_info())) + self.publish('log', msg, level) + +bus = Bus() diff --git a/libs/cherrypy/scaffold/__init__.py b/libs/cherrypy/scaffold/__init__.py new file mode 100644 index 0000000..00964ac --- /dev/null +++ b/libs/cherrypy/scaffold/__init__.py @@ -0,0 +1,61 @@ +""", a CherryPy application. + +Use this as a base for creating new CherryPy applications. When you want +to make a new app, copy and paste this folder to some other location +(maybe site-packages) and rename it to the name of your project, +then tweak as desired. + +Even before any tweaking, this should serve a few demonstration pages. +Change to this directory and run: + + ../cherryd -c site.conf + +""" + +import cherrypy +from cherrypy import tools, url + +import os +local_dir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +class Root: + + _cp_config = {'tools.log_tracebacks.on': True, + } + + def index(self): + return """ +Try some other path, +or a default path.
+Or, just look at the pretty picture:
+ +""" % (url("other"), url("else"), + url("files/made_with_cherrypy_small.png")) + index.exposed = True + + def default(self, *args, **kwargs): + return "args: %s kwargs: %s" % (args, kwargs) + default.exposed = True + + def other(self, a=2, b='bananas', c=None): + cherrypy.response.headers['Content-Type'] = 'text/plain' + if c is None: + return "Have %d %s." % (int(a), b) + else: + return "Have %d %s, %s." % (int(a), b, c) + other.exposed = True + + files = cherrypy.tools.staticdir.handler( + section="/files", + dir=os.path.join(local_dir, "static"), + # Ignore .php files, etc. + match=r'\.(css|gif|html?|ico|jpe?g|js|png|swf|xml)$', + ) + + +root = Root() + +# Uncomment the following to use your own favicon instead of CP's default. +#favicon_path = os.path.join(local_dir, "favicon.ico") +#root.favicon_ico = tools.staticfile.handler(filename=favicon_path) diff --git a/libs/cherrypy/scaffold/example.conf b/libs/cherrypy/scaffold/example.conf new file mode 100644 index 0000000..93a6e53 --- /dev/null +++ b/libs/cherrypy/scaffold/example.conf @@ -0,0 +1,3 @@ +[/] +log.error_file: "error.log" +log.access_file: "access.log" \ No newline at end of file diff --git a/libs/cherrypy/scaffold/site.conf b/libs/cherrypy/scaffold/site.conf new file mode 100644 index 0000000..6ed3898 --- /dev/null +++ b/libs/cherrypy/scaffold/site.conf @@ -0,0 +1,14 @@ +[global] +# Uncomment this when you're done developing +#environment: "production" + +server.socket_host: "0.0.0.0" +server.socket_port: 8088 + +# Uncomment the following lines to run on HTTPS at the same time +#server.2.socket_host: "0.0.0.0" +#server.2.socket_port: 8433 +#server.2.ssl_certificate: '../test/test.pem' +#server.2.ssl_private_key: '../test/test.pem' + +tree.myapp: cherrypy.Application(scaffold.root, "/", "example.conf") diff --git a/libs/cherrypy/scaffold/static/made_with_cherrypy_small.png b/libs/cherrypy/scaffold/static/made_with_cherrypy_small.png new file mode 100644 index 0000000000000000000000000000000000000000..c3aafeed952190f5da9982bb359aa75b107ff079 GIT binary patch literal 7455 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ξb^d7?(E1>b^p1aAHqz^4_gi*iN74 z2x=0ye3f^&T`3}*uTMEcbiU`b9g;sI;+6KiNcf)^`nsXi-frS=FWtBM%%{s+q-b~@ zzS-J5?eXc=Mqf16M22y!^XWR56S3YVRDA#0Kl8L_{WROWyuac4&9f(VI|qvL{&;>o zQ^7W9=JKg3?~nUWX64(uQZJH2u;|4KooD~9?6_FVx;=P>k7}$)#Qr3m_22Dsxihr> zv|T9>yK-DFnB!N_oF%JJ!*fFDQ@2>}*OR}b4JwW1pG%T?ajc)^S6)Qh*MrZucYb(f$zc93 zf{TBt{c8u+x;tGjgWP9q7i;YLH^nKNZ(-!k5ZUJGY}4ZBzc08x^WEuGqodw_3m4u_ zc)Y9S-QJfbE5oO(=xr@#aYzgfT_0z=q2Qj$o7>ul&23DlTu(l{>*|HADUj(SE( z*t;>f3C~ScdKz$ZbHisTyDjXD-RoMeo{aL?cmE1!(_fWs+IEG9G;*d#bM@9{FVNY~ zHrwn^ji!CZ+U%Iy>xopSK*yCXEM+4 zbhcZ-5xcBSTPZKkyn{$)^^OPTkkHs}Lw%J>LEApQ4 z=%=sK6PdBTE&hw5ja+LZZKBuaE&v*BX457w2ur$sJ?uiRB` zx9``Y)df=P@*jV_o_SR5=rVuhIQ92^TDjZ4Je|w*ywjDH3n0zB+HP><-J_d+zZ|tC{w>ffl}hCjLsfq|<+> zqVz}XozCO^D!;u=l#=gB?g;wI^YH%n>drOc4O2um`P=i~nEvIEQ}*f5R4FC(YaM)3 zuReIRLHgXc-4EU^>S-xRj`E$woxIIEsQ2ZE>yzTS#jmnOTWX z2{*4F*eP6HVJ+^b`5=wquKJmrnB^*0r+i+uAeXQ3uV${}Jn`3a%Okq|?Ot*z&z+i7 zX|&V(<(35|ce`#)vzfL(G1~B*h~MAGE)}P|yRSLlJvw=kN@+{XMCY)q6)xAjmK@EB z_?(z0cjI5!oolgMB(<)xq`bMI7U|4hzWBVNZp=Kt3z2tBxqrT0F#W1y^^=mBzt}Z{ zt}wY?-}CZN*56kP#401dEbn|#_t171#{oV5Eum+={x`aBbZPeN%V}q3S@H&%91Z)F zx$9}S{94_^NspTKa-2Kfu+5RtYBjp0QajOIGxmZ9_xYi`$B72R>-M6(* ze{q-GWIgXJqOG=@^Pb4+r@Oq_W(IQYncH*1Ho@?%`Q9f5XDel$_M8m}oOWBwJ2d{s zRHoJQG;crOFNe}-& zMX^1WD^EndzT}tBule?OTHYRMnd^|F5<6cx{#ByyQ*XvqzKvh|Rese+{Jgn2-N4+q zwae(Q<>J-vDt>Kw`!A{F&CJUmE?jeaa + + CherryPy Benchmark + + + + +""" + index.exposed = True + + def hello(self): + return "Hello, world\r\n" + hello.exposed = True + + def sizer(self, size): + resp = size_cache.get(size, None) + if resp is None: + size_cache[size] = resp = "X" * int(size) + return resp + sizer.exposed = True + + +cherrypy.config.update({ + 'log.error.file': '', + 'environment': 'production', + 'server.socket_host': '127.0.0.1', + 'server.socket_port': 54583, + 'server.max_request_header_size': 0, + 'server.max_request_body_size': 0, + 'engine.deadlock_poll_freq': 0, + }) + +# Cheat mode on ;) +del cherrypy.config['tools.log_tracebacks.on'] +del cherrypy.config['tools.log_headers.on'] +del cherrypy.config['tools.trailing_slash.on'] + +appconf = { + '/static': { + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.root': curdir, + }, + } +app = cherrypy.tree.mount(Root(), SCRIPT_NAME, appconf) + + +class NullRequest: + """A null HTTP request class, returning 200 and an empty body.""" + + def __init__(self, local, remote, scheme="http"): + pass + + def close(self): + pass + + def run(self, method, path, query_string, protocol, headers, rfile): + cherrypy.response.status = "200 OK" + cherrypy.response.header_list = [("Content-Type", 'text/html'), + ("Server", "Null CherryPy"), + ("Date", httputil.HTTPDate()), + ("Content-Length", "0"), + ] + cherrypy.response.body = [""] + return cherrypy.response + + +class NullResponse: + pass + + +class ABSession: + """A session of 'ab', the Apache HTTP server benchmarking tool. + +Example output from ab: + +This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.1 $> apache-2.0 +Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/ + +Benchmarking 127.0.0.1 (be patient) +Completed 100 requests +Completed 200 requests +Completed 300 requests +Completed 400 requests +Completed 500 requests +Completed 600 requests +Completed 700 requests +Completed 800 requests +Completed 900 requests + + +Server Software: CherryPy/3.1beta +Server Hostname: 127.0.0.1 +Server Port: 54583 + +Document Path: /static/index.html +Document Length: 14 bytes + +Concurrency Level: 10 +Time taken for tests: 9.643867 seconds +Complete requests: 1000 +Failed requests: 0 +Write errors: 0 +Total transferred: 189000 bytes +HTML transferred: 14000 bytes +Requests per second: 103.69 [#/sec] (mean) +Time per request: 96.439 [ms] (mean) +Time per request: 9.644 [ms] (mean, across all concurrent requests) +Transfer rate: 19.08 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 2.9 0 10 +Processing: 20 94 7.3 90 130 +Waiting: 0 43 28.1 40 100 +Total: 20 95 7.3 100 130 + +Percentage of the requests served within a certain time (ms) + 50% 100 + 66% 100 + 75% 100 + 80% 100 + 90% 100 + 95% 100 + 98% 100 + 99% 110 + 100% 130 (longest request) +Finished 1000 requests +""" + + parse_patterns = [('complete_requests', 'Completed', + ntob(r'^Complete requests:\s*(\d+)')), + ('failed_requests', 'Failed', + ntob(r'^Failed requests:\s*(\d+)')), + ('requests_per_second', 'req/sec', + ntob(r'^Requests per second:\s*([0-9.]+)')), + ('time_per_request_concurrent', 'msec/req', + ntob(r'^Time per request:\s*([0-9.]+).*concurrent requests\)$')), + ('transfer_rate', 'KB/sec', + ntob(r'^Transfer rate:\s*([0-9.]+)')), + ] + + def __init__(self, path=SCRIPT_NAME + "/hello", requests=1000, concurrency=10): + self.path = path + self.requests = requests + self.concurrency = concurrency + + def args(self): + port = cherrypy.server.socket_port + assert self.concurrency > 0 + assert self.requests > 0 + # Don't use "localhost". + # Cf http://mail.python.org/pipermail/python-win32/2008-March/007050.html + return ("-k -n %s -c %s http://127.0.0.1:%s%s" % + (self.requests, self.concurrency, port, self.path)) + + def run(self): + # Parse output of ab, setting attributes on self + try: + self.output = _cpmodpy.read_process(AB_PATH or "ab", self.args()) + except: + print(_cperror.format_exc()) + raise + + for attr, name, pattern in self.parse_patterns: + val = re.search(pattern, self.output, re.MULTILINE) + if val: + val = val.group(1) + setattr(self, attr, val) + else: + setattr(self, attr, None) + + +safe_threads = (25, 50, 100, 200, 400) +if sys.platform in ("win32",): + # For some reason, ab crashes with > 50 threads on my Win2k laptop. + safe_threads = (10, 20, 30, 40, 50) + + +def thread_report(path=SCRIPT_NAME + "/hello", concurrency=safe_threads): + sess = ABSession(path) + attrs, names, patterns = list(zip(*sess.parse_patterns)) + avg = dict.fromkeys(attrs, 0.0) + + yield ('threads',) + names + for c in concurrency: + sess.concurrency = c + sess.run() + row = [c] + for attr in attrs: + val = getattr(sess, attr) + if val is None: + print(sess.output) + row = None + break + val = float(val) + avg[attr] += float(val) + row.append(val) + if row: + yield row + + # Add a row of averages. + yield ["Average"] + [str(avg[attr] / len(concurrency)) for attr in attrs] + +def size_report(sizes=(10, 100, 1000, 10000, 100000, 100000000), + concurrency=50): + sess = ABSession(concurrency=concurrency) + attrs, names, patterns = list(zip(*sess.parse_patterns)) + yield ('bytes',) + names + for sz in sizes: + sess.path = "%s/sizer?size=%s" % (SCRIPT_NAME, sz) + sess.run() + yield [sz] + [getattr(sess, attr) for attr in attrs] + +def print_report(rows): + for row in rows: + print("") + for i, val in enumerate(row): + sys.stdout.write(str(val).rjust(10) + " | ") + print("") + + +def run_standard_benchmarks(): + print("") + print("Client Thread Report (1000 requests, 14 byte response body, " + "%s server threads):" % cherrypy.server.thread_pool) + print_report(thread_report()) + + print("") + print("Client Thread Report (1000 requests, 14 bytes via staticdir, " + "%s server threads):" % cherrypy.server.thread_pool) + print_report(thread_report("%s/static/index.html" % SCRIPT_NAME)) + + print("") + print("Size Report (1000 requests, 50 client threads, " + "%s server threads):" % cherrypy.server.thread_pool) + print_report(size_report()) + + +# modpython and other WSGI # + +def startup_modpython(req=None): + """Start the CherryPy app server in 'serverless' mode (for modpython/WSGI).""" + if cherrypy.engine.state == cherrypy._cpengine.STOPPED: + if req: + if "nullreq" in req.get_options(): + cherrypy.engine.request_class = NullRequest + cherrypy.engine.response_class = NullResponse + ab_opt = req.get_options().get("ab", "") + if ab_opt: + global AB_PATH + AB_PATH = ab_opt + cherrypy.engine.start() + if cherrypy.engine.state == cherrypy._cpengine.STARTING: + cherrypy.engine.wait() + return 0 # apache.OK + + +def run_modpython(use_wsgi=False): + print("Starting mod_python...") + pyopts = [] + + # Pass the null and ab=path options through Apache + if "--null" in opts: + pyopts.append(("nullreq", "")) + + if "--ab" in opts: + pyopts.append(("ab", opts["--ab"])) + + s = _cpmodpy.ModPythonServer + if use_wsgi: + pyopts.append(("wsgi.application", "cherrypy::tree")) + pyopts.append(("wsgi.startup", "cherrypy.test.benchmark::startup_modpython")) + handler = "modpython_gateway::handler" + s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH, handler=handler) + else: + pyopts.append(("cherrypy.setup", "cherrypy.test.benchmark::startup_modpython")) + s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH) + + try: + s.start() + run() + finally: + s.stop() + + + +if __name__ == '__main__': + longopts = ['cpmodpy', 'modpython', 'null', 'notests', + 'help', 'ab=', 'apache='] + try: + switches, args = getopt.getopt(sys.argv[1:], "", longopts) + opts = dict(switches) + except getopt.GetoptError: + print(__doc__) + sys.exit(2) + + if "--help" in opts: + print(__doc__) + sys.exit(0) + + if "--ab" in opts: + AB_PATH = opts['--ab'] + + if "--notests" in opts: + # Return without stopping the server, so that the pages + # can be tested from a standard web browser. + def run(): + port = cherrypy.server.socket_port + print("You may now open http://127.0.0.1:%s%s/" % + (port, SCRIPT_NAME)) + + if "--null" in opts: + print("Using null Request object") + else: + def run(): + end = time.time() - start + print("Started in %s seconds" % end) + if "--null" in opts: + print("\nUsing null Request object") + try: + try: + run_standard_benchmarks() + except: + print(_cperror.format_exc()) + raise + finally: + cherrypy.engine.exit() + + print("Starting CherryPy app server...") + + class NullWriter(object): + """Suppresses the printing of socket errors.""" + def write(self, data): + pass + sys.stderr = NullWriter() + + start = time.time() + + if "--cpmodpy" in opts: + run_modpython() + elif "--modpython" in opts: + run_modpython(use_wsgi=True) + else: + if "--null" in opts: + cherrypy.server.request_class = NullRequest + cherrypy.server.response_class = NullResponse + + cherrypy.engine.start_with_callback(run) + cherrypy.engine.block() diff --git a/libs/cherrypy/test/checkerdemo.py b/libs/cherrypy/test/checkerdemo.py new file mode 100644 index 0000000..32a7dee --- /dev/null +++ b/libs/cherrypy/test/checkerdemo.py @@ -0,0 +1,47 @@ +"""Demonstration app for cherrypy.checker. + +This application is intentionally broken and badly designed. +To demonstrate the output of the CherryPy Checker, simply execute +this module. +""" + +import os +import cherrypy +thisdir = os.path.dirname(os.path.abspath(__file__)) + +class Root: + pass + +if __name__ == '__main__': + conf = {'/base': {'tools.staticdir.root': thisdir, + # Obsolete key. + 'throw_errors': True, + }, + # This entry should be OK. + '/base/static': {'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static'}, + # Warn on missing folder. + '/base/js': {'tools.staticdir.on': True, + 'tools.staticdir.dir': 'js'}, + # Warn on dir with an abs path even though we provide root. + '/base/static2': {'tools.staticdir.on': True, + 'tools.staticdir.dir': '/static'}, + # Warn on dir with a relative path with no root. + '/static3': {'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static'}, + # Warn on unknown namespace + '/unknown': {'toobles.gzip.on': True}, + # Warn special on cherrypy..* + '/cpknown': {'cherrypy.tools.encode.on': True}, + # Warn on mismatched types + '/conftype': {'request.show_tracebacks': 14}, + # Warn on unknown tool. + '/web': {'tools.unknown.on': True}, + # Warn on server.* in app config. + '/app1': {'server.socket_host': '0.0.0.0'}, + # Warn on 'localhost' + 'global': {'server.socket_host': 'localhost'}, + # Warn on '[name]' + '[/extra_brackets]': {}, + } + cherrypy.quickstart(Root(), config=conf) diff --git a/libs/cherrypy/test/helper.py b/libs/cherrypy/test/helper.py new file mode 100644 index 0000000..e3006a5 --- /dev/null +++ b/libs/cherrypy/test/helper.py @@ -0,0 +1,494 @@ +"""A library of helper functions for the CherryPy test suite.""" + +import datetime +import logging +log = logging.getLogger(__name__) +import os +thisdir = os.path.abspath(os.path.dirname(__file__)) +serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem') + +import re +import sys +import time +import warnings + +import cherrypy +from cherrypy._cpcompat import basestring, copyitems, HTTPSConnection, ntob +from cherrypy.lib import httputil +from cherrypy.lib import gctools +from cherrypy.lib.reprconf import unrepr +from cherrypy.test import webtest + +import nose + +_testconfig = None + +def get_tst_config(overconf = {}): + global _testconfig + if _testconfig is None: + conf = { + 'scheme': 'http', + 'protocol': "HTTP/1.1", + 'port': 54583, + 'host': '127.0.0.1', + 'validate': False, + 'conquer': False, + 'server': 'wsgi', + } + try: + import testconfig + _conf = testconfig.config.get('supervisor', None) + if _conf is not None: + for k, v in _conf.items(): + if isinstance(v, basestring): + _conf[k] = unrepr(v) + conf.update(_conf) + except ImportError: + pass + _testconfig = conf + conf = _testconfig.copy() + conf.update(overconf) + + return conf + +class Supervisor(object): + """Base class for modeling and controlling servers during testing.""" + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + if k == 'port': + setattr(self, k, int(v)) + setattr(self, k, v) + + +log_to_stderr = lambda msg, level: sys.stderr.write(msg + os.linesep) + +class LocalSupervisor(Supervisor): + """Base class for modeling/controlling servers which run in the same process. + + When the server side runs in a different process, start/stop can dump all + state between each test module easily. When the server side runs in the + same process as the client, however, we have to do a bit more work to ensure + config and mounted apps are reset between tests. + """ + + using_apache = False + using_wsgi = False + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + cherrypy.server.httpserver = self.httpserver_class + + # This is perhaps the wrong place for this call but this is the only + # place that i've found so far that I KNOW is early enough to set this. + cherrypy.config.update({'log.screen': False}) + engine = cherrypy.engine + if hasattr(engine, "signal_handler"): + engine.signal_handler.subscribe() + if hasattr(engine, "console_control_handler"): + engine.console_control_handler.subscribe() + #engine.subscribe('log', log_to_stderr) + + def start(self, modulename=None): + """Load and start the HTTP server.""" + if modulename: + # Unhook httpserver so cherrypy.server.start() creates a new + # one (with config from setup_server, if declared). + cherrypy.server.httpserver = None + + cherrypy.engine.start() + + self.sync_apps() + + def sync_apps(self): + """Tell the server about any apps which the setup functions mounted.""" + pass + + def stop(self): + td = getattr(self, 'teardown', None) + if td: + td() + + cherrypy.engine.exit() + + for name, server in copyitems(getattr(cherrypy, 'servers', {})): + server.unsubscribe() + del cherrypy.servers[name] + + +class NativeServerSupervisor(LocalSupervisor): + """Server supervisor for the builtin HTTP server.""" + + httpserver_class = "cherrypy._cpnative_server.CPHTTPServer" + using_apache = False + using_wsgi = False + + def __str__(self): + return "Builtin HTTP Server on %s:%s" % (self.host, self.port) + + +class LocalWSGISupervisor(LocalSupervisor): + """Server supervisor for the builtin WSGI server.""" + + httpserver_class = "cherrypy._cpwsgi_server.CPWSGIServer" + using_apache = False + using_wsgi = True + + def __str__(self): + return "Builtin WSGI Server on %s:%s" % (self.host, self.port) + + def sync_apps(self): + """Hook a new WSGI app into the origin server.""" + cherrypy.server.httpserver.wsgi_app = self.get_app() + + def get_app(self, app=None): + """Obtain a new (decorated) WSGI app to hook into the origin server.""" + if app is None: + app = cherrypy.tree + + if self.conquer: + try: + import wsgiconq + except ImportError: + warnings.warn("Error importing wsgiconq. pyconquer will not run.") + else: + app = wsgiconq.WSGILogger(app, c_calls=True) + + if self.validate: + try: + from wsgiref import validate + except ImportError: + warnings.warn("Error importing wsgiref. The validator will not run.") + else: + #wraps the app in the validator + app = validate.validator(app) + + return app + + +def get_cpmodpy_supervisor(**options): + from cherrypy.test import modpy + sup = modpy.ModPythonSupervisor(**options) + sup.template = modpy.conf_cpmodpy + return sup + +def get_modpygw_supervisor(**options): + from cherrypy.test import modpy + sup = modpy.ModPythonSupervisor(**options) + sup.template = modpy.conf_modpython_gateway + sup.using_wsgi = True + return sup + +def get_modwsgi_supervisor(**options): + from cherrypy.test import modwsgi + return modwsgi.ModWSGISupervisor(**options) + +def get_modfcgid_supervisor(**options): + from cherrypy.test import modfcgid + return modfcgid.ModFCGISupervisor(**options) + +def get_modfastcgi_supervisor(**options): + from cherrypy.test import modfastcgi + return modfastcgi.ModFCGISupervisor(**options) + +def get_wsgi_u_supervisor(**options): + cherrypy.server.wsgi_version = ('u', 0) + return LocalWSGISupervisor(**options) + + +class CPWebCase(webtest.WebCase): + + script_name = "" + scheme = "http" + + available_servers = {'wsgi': LocalWSGISupervisor, + 'wsgi_u': get_wsgi_u_supervisor, + 'native': NativeServerSupervisor, + 'cpmodpy': get_cpmodpy_supervisor, + 'modpygw': get_modpygw_supervisor, + 'modwsgi': get_modwsgi_supervisor, + 'modfcgid': get_modfcgid_supervisor, + 'modfastcgi': get_modfastcgi_supervisor, + } + default_server = "wsgi" + + def _setup_server(cls, supervisor, conf): + v = sys.version.split()[0] + log.info("Python version used to run this test script: %s" % v) + log.info("CherryPy version: %s" % cherrypy.__version__) + if supervisor.scheme == "https": + ssl = " (ssl)" + else: + ssl = "" + log.info("HTTP server version: %s%s" % (supervisor.protocol, ssl)) + log.info("PID: %s" % os.getpid()) + + cherrypy.server.using_apache = supervisor.using_apache + cherrypy.server.using_wsgi = supervisor.using_wsgi + + if sys.platform[:4] == 'java': + cherrypy.config.update({'server.nodelay': False}) + + if isinstance(conf, basestring): + parser = cherrypy.lib.reprconf.Parser() + conf = parser.dict_from_file(conf).get('global', {}) + else: + conf = conf or {} + baseconf = conf.copy() + baseconf.update({'server.socket_host': supervisor.host, + 'server.socket_port': supervisor.port, + 'server.protocol_version': supervisor.protocol, + 'environment': "test_suite", + }) + if supervisor.scheme == "https": + #baseconf['server.ssl_module'] = 'builtin' + baseconf['server.ssl_certificate'] = serverpem + baseconf['server.ssl_private_key'] = serverpem + + # helper must be imported lazily so the coverage tool + # can run against module-level statements within cherrypy. + # Also, we have to do "from cherrypy.test import helper", + # exactly like each test module does, because a relative import + # would stick a second instance of webtest in sys.modules, + # and we wouldn't be able to globally override the port anymore. + if supervisor.scheme == "https": + webtest.WebCase.HTTP_CONN = HTTPSConnection + return baseconf + _setup_server = classmethod(_setup_server) + + def setup_class(cls): + '' + #Creates a server + conf = get_tst_config() + supervisor_factory = cls.available_servers.get(conf.get('server', 'wsgi')) + if supervisor_factory is None: + raise RuntimeError('Unknown server in config: %s' % conf['server']) + supervisor = supervisor_factory(**conf) + + #Copied from "run_test_suite" + cherrypy.config.reset() + baseconf = cls._setup_server(supervisor, conf) + cherrypy.config.update(baseconf) + setup_client() + + if hasattr(cls, 'setup_server'): + # Clear the cherrypy tree and clear the wsgi server so that + # it can be updated with the new root + cherrypy.tree = cherrypy._cptree.Tree() + cherrypy.server.httpserver = None + cls.setup_server() + # Add a resource for verifying there are no refleaks + # to *every* test class. + cherrypy.tree.mount(gctools.GCRoot(), '/gc') + cls.do_gc_test = True + supervisor.start(cls.__module__) + + cls.supervisor = supervisor + setup_class = classmethod(setup_class) + + def teardown_class(cls): + '' + if hasattr(cls, 'setup_server'): + cls.supervisor.stop() + teardown_class = classmethod(teardown_class) + + do_gc_test = False + + def test_gc(self): + if self.do_gc_test: + self.getPage("/gc/stats") + self.assertBody("Statistics:") + # Tell nose to run this last in each class. + # Prefer sys.maxint for Python 2.3, which didn't have float('inf') + test_gc.compat_co_firstlineno = getattr(sys, 'maxint', None) or float('inf') + + def prefix(self): + return self.script_name.rstrip("/") + + def base(self): + if ((self.scheme == "http" and self.PORT == 80) or + (self.scheme == "https" and self.PORT == 443)): + port = "" + else: + port = ":%s" % self.PORT + + return "%s://%s%s%s" % (self.scheme, self.HOST, port, + self.script_name.rstrip("/")) + + def exit(self): + sys.exit() + + def getPage(self, url, headers=None, method="GET", body=None, protocol=None): + """Open the url. Return status, headers, body.""" + if self.script_name: + url = httputil.urljoin(self.script_name, url) + return webtest.WebCase.getPage(self, url, headers, method, body, protocol) + + def skip(self, msg='skipped '): + raise nose.SkipTest(msg) + + def assertErrorPage(self, status, message=None, pattern=''): + """Compare the response body with a built in error page. + + The function will optionally look for the regexp pattern, + within the exception embedded in the error page.""" + + # This will never contain a traceback + page = cherrypy._cperror.get_error_page(status, message=message) + + # First, test the response body without checking the traceback. + # Stick a match-all group (.*) in to grab the traceback. + esc = re.escape + epage = esc(page) + epage = epage.replace(esc('
'),
+                              esc('
') + '(.*)' + esc('
')) + m = re.match(ntob(epage, self.encoding), self.body, re.DOTALL) + if not m: + self._handlewebError('Error page does not match; expected:\n' + page) + return + + # Now test the pattern against the traceback + if pattern is None: + # Special-case None to mean that there should be *no* traceback. + if m and m.group(1): + self._handlewebError('Error page contains traceback') + else: + if (m is None) or ( + not re.search(ntob(re.escape(pattern), self.encoding), + m.group(1))): + msg = 'Error page does not contain %s in traceback' + self._handlewebError(msg % repr(pattern)) + + date_tolerance = 2 + + def assertEqualDates(self, dt1, dt2, seconds=None): + """Assert abs(dt1 - dt2) is within Y seconds.""" + if seconds is None: + seconds = self.date_tolerance + + if dt1 > dt2: + diff = dt1 - dt2 + else: + diff = dt2 - dt1 + if not diff < datetime.timedelta(seconds=seconds): + raise AssertionError('%r and %r are not within %r seconds.' % + (dt1, dt2, seconds)) + + +def setup_client(): + """Set up the WebCase classes to match the server's socket settings.""" + webtest.WebCase.PORT = cherrypy.server.socket_port + webtest.WebCase.HOST = cherrypy.server.socket_host + if cherrypy.server.ssl_certificate: + CPWebCase.scheme = 'https' + +# --------------------------- Spawning helpers --------------------------- # + + +class CPProcess(object): + + pid_file = os.path.join(thisdir, 'test.pid') + config_file = os.path.join(thisdir, 'test.conf') + config_template = """[global] +server.socket_host: '%(host)s' +server.socket_port: %(port)s +checker.on: False +log.screen: False +log.error_file: r'%(error_log)s' +log.access_file: r'%(access_log)s' +%(ssl)s +%(extra)s +""" + error_log = os.path.join(thisdir, 'test.error.log') + access_log = os.path.join(thisdir, 'test.access.log') + + def __init__(self, wait=False, daemonize=False, ssl=False, socket_host=None, socket_port=None): + self.wait = wait + self.daemonize = daemonize + self.ssl = ssl + self.host = socket_host or cherrypy.server.socket_host + self.port = socket_port or cherrypy.server.socket_port + + def write_conf(self, extra=""): + if self.ssl: + serverpem = os.path.join(thisdir, 'test.pem') + ssl = """ +server.ssl_certificate: r'%s' +server.ssl_private_key: r'%s' +""" % (serverpem, serverpem) + else: + ssl = "" + + conf = self.config_template % { + 'host': self.host, + 'port': self.port, + 'error_log': self.error_log, + 'access_log': self.access_log, + 'ssl': ssl, + 'extra': extra, + } + f = open(self.config_file, 'wb') + f.write(ntob(conf, 'utf-8')) + f.close() + + def start(self, imports=None): + """Start cherryd in a subprocess.""" + cherrypy._cpserver.wait_for_free_port(self.host, self.port) + + args = [sys.executable, os.path.join(thisdir, '..', 'cherryd'), + '-c', self.config_file, '-p', self.pid_file] + + if not isinstance(imports, (list, tuple)): + imports = [imports] + for i in imports: + if i: + args.append('-i') + args.append(i) + + if self.daemonize: + args.append('-d') + + env = os.environ.copy() + # Make sure we import the cherrypy package in which this module is defined. + grandparentdir = os.path.abspath(os.path.join(thisdir, '..', '..')) + if env.get('PYTHONPATH', ''): + env['PYTHONPATH'] = os.pathsep.join((grandparentdir, env['PYTHONPATH'])) + else: + env['PYTHONPATH'] = grandparentdir + if self.wait: + self.exit_code = os.spawnve(os.P_WAIT, sys.executable, args, env) + else: + os.spawnve(os.P_NOWAIT, sys.executable, args, env) + cherrypy._cpserver.wait_for_occupied_port(self.host, self.port) + + # Give the engine a wee bit more time to finish STARTING + if self.daemonize: + time.sleep(2) + else: + time.sleep(1) + + def get_pid(self): + return int(open(self.pid_file, 'rb').read()) + + def join(self): + """Wait for the process to exit.""" + try: + try: + # Mac, UNIX + os.wait() + except AttributeError: + # Windows + try: + pid = self.get_pid() + except IOError: + # Assume the subprocess deleted the pidfile on shutdown. + pass + else: + os.waitpid(pid, 0) + except OSError: + x = sys.exc_info()[1] + if x.args != (10, 'No child processes'): + raise + diff --git a/libs/cherrypy/test/logtest.py b/libs/cherrypy/test/logtest.py new file mode 100644 index 0000000..3c6f114 --- /dev/null +++ b/libs/cherrypy/test/logtest.py @@ -0,0 +1,188 @@ +"""logtest, a unittest.TestCase helper for testing log output.""" + +import sys +import time + +import cherrypy +from cherrypy._cpcompat import basestring, ntob, unicodestr + + +try: + # On Windows, msvcrt.getch reads a single char without output. + import msvcrt + def getchar(): + return msvcrt.getch() +except ImportError: + # Unix getchr + import tty, termios + def getchar(): + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch + + +class LogCase(object): + """unittest.TestCase mixin for testing log messages. + + logfile: a filename for the desired log. Yes, I know modes are evil, + but it makes the test functions so much cleaner to set this once. + + lastmarker: the last marker in the log. This can be used to search for + messages since the last marker. + + markerPrefix: a string with which to prefix log markers. This should be + unique enough from normal log output to use for marker identification. + """ + + logfile = None + lastmarker = None + markerPrefix = ntob("test suite marker: ") + + def _handleLogError(self, msg, data, marker, pattern): + print("") + print(" ERROR: %s" % msg) + + if not self.interactive: + raise self.failureException(msg) + + p = " Show: [L]og [M]arker [P]attern; [I]gnore, [R]aise, or sys.e[X]it >> " + sys.stdout.write(p + ' ') + # ARGH + sys.stdout.flush() + while True: + i = getchar().upper() + if i not in "MPLIRX": + continue + print(i.upper()) # Also prints new line + if i == "L": + for x, line in enumerate(data): + if (x + 1) % self.console_height == 0: + # The \r and comma should make the next line overwrite + sys.stdout.write("<-- More -->\r ") + m = getchar().lower() + # Erase our "More" prompt + sys.stdout.write(" \r ") + if m == "q": + break + print(line.rstrip()) + elif i == "M": + print(repr(marker or self.lastmarker)) + elif i == "P": + print(repr(pattern)) + elif i == "I": + # return without raising the normal exception + return + elif i == "R": + raise self.failureException(msg) + elif i == "X": + self.exit() + sys.stdout.write(p + ' ') + + def exit(self): + sys.exit() + + def emptyLog(self): + """Overwrite self.logfile with 0 bytes.""" + open(self.logfile, 'wb').write("") + + def markLog(self, key=None): + """Insert a marker line into the log and set self.lastmarker.""" + if key is None: + key = str(time.time()) + self.lastmarker = key + + open(self.logfile, 'ab+').write(ntob("%s%s\n" % (self.markerPrefix, key),"utf-8")) + + def _read_marked_region(self, marker=None): + """Return lines from self.logfile in the marked region. + + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be returned. + """ +## # Give the logger time to finish writing? +## time.sleep(0.5) + + logfile = self.logfile + marker = marker or self.lastmarker + if marker is None: + return open(logfile, 'rb').readlines() + + if isinstance(marker, unicodestr): + marker = marker.encode('utf-8') + data = [] + in_region = False + for line in open(logfile, 'rb'): + if in_region: + if (line.startswith(self.markerPrefix) and not marker in line): + break + else: + data.append(line) + elif marker in line: + in_region = True + return data + + def assertInLog(self, line, marker=None): + """Fail if the given (partial) line is not in the log. + + The log will be searched from the given marker to the next marker. + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be searched. + """ + data = self._read_marked_region(marker) + for logline in data: + if line in logline: + return + msg = "%r not found in log" % line + self._handleLogError(msg, data, marker, line) + + def assertNotInLog(self, line, marker=None): + """Fail if the given (partial) line is in the log. + + The log will be searched from the given marker to the next marker. + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be searched. + """ + data = self._read_marked_region(marker) + for logline in data: + if line in logline: + msg = "%r found in log" % line + self._handleLogError(msg, data, marker, line) + + def assertLog(self, sliceargs, lines, marker=None): + """Fail if log.readlines()[sliceargs] is not contained in 'lines'. + + The log will be searched from the given marker to the next marker. + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be searched. + """ + data = self._read_marked_region(marker) + if isinstance(sliceargs, int): + # Single arg. Use __getitem__ and allow lines to be str or list. + if isinstance(lines, (tuple, list)): + lines = lines[0] + if isinstance(lines, unicodestr): + lines = lines.encode('utf-8') + if lines not in data[sliceargs]: + msg = "%r not found on log line %r" % (lines, sliceargs) + self._handleLogError(msg, [data[sliceargs],"--EXTRA CONTEXT--"] + data[sliceargs+1:sliceargs+6], marker, lines) + else: + # Multiple args. Use __getslice__ and require lines to be list. + if isinstance(lines, tuple): + lines = list(lines) + elif isinstance(lines, basestring): + raise TypeError("The 'lines' arg must be a list when " + "'sliceargs' is a tuple.") + + start, stop = sliceargs + for line, logline in zip(lines, data[start:stop]): + if isinstance(line, unicodestr): + line = line.encode('utf-8') + if line not in logline: + msg = "%r not found in log" % line + self._handleLogError(msg, data[start:stop], marker, line) + diff --git a/libs/cherrypy/test/modfastcgi.py b/libs/cherrypy/test/modfastcgi.py new file mode 100644 index 0000000..95acf14 --- /dev/null +++ b/libs/cherrypy/test/modfastcgi.py @@ -0,0 +1,135 @@ +"""Wrapper for mod_fastcgi, for use as a CherryPy HTTP server when testing. + +To autostart fastcgi, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl", "apache2ctl", +or "httpd"--create a symlink to them if needed. + +You'll also need the WSGIServer from flup.servers. +See http://projects.amor.org/misc/wiki/ModPythonGateway + + +KNOWN BUGS +========== + +1. Apache processes Range headers automatically; CherryPy's truncated + output is then truncated again by Apache. See test_core.testRanges. + This was worked around in http://www.cherrypy.org/changeset/1319. +2. Apache does not allow custom HTTP methods like CONNECT as per the spec. + See test_core.testHTTPMethods. +3. Max request header and body settings do not work with Apache. +4. Apache replaces status "reason phrases" automatically. For example, + CherryPy may set "304 Not modified" but Apache will write out + "304 Not Modified" (capital "M"). +5. Apache does not allow custom error codes as per the spec. +6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the + Request-URI too early. +7. mod_python will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. Apache will output a "Content-Length: 0" response header even if there's + no response entity body. This isn't really a bug; it just differs from + the CherryPy default. +""" + +import os +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) +import re +import sys +import time + +import cherrypy +from cherrypy.process import plugins, servers +from cherrypy.test import helper + + +def read_process(cmd, args=""): + pipein, pipeout = os.popen4("%s %s" % (cmd, args)) + try: + firstline = pipeout.readline() + if (re.search(r"(not recognized|No such file|not found)", firstline, + re.IGNORECASE)): + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +APACHE_PATH = "apache2ctl" +CONF_PATH = "fastcgi.conf" + +conf_fastcgi = """ +# Apache2 server conf file for testing CherryPy with mod_fastcgi. +# fumanchu: I had to hard-code paths due to crazy Debian layouts :( +ServerRoot /usr/lib/apache2 +User #1000 +ErrorLog %(root)s/mod_fastcgi.error.log + +DocumentRoot "%(root)s" +ServerName 127.0.0.1 +Listen %(port)s +LoadModule fastcgi_module modules/mod_fastcgi.so +LoadModule rewrite_module modules/mod_rewrite.so + +Options +ExecCGI +SetHandler fastcgi-script +RewriteEngine On +RewriteRule ^(.*)$ /fastcgi.pyc [L] +FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000 +""" + +def erase_script_name(environ, start_response): + environ['SCRIPT_NAME'] = '' + return cherrypy.tree(environ, start_response) + +class ModFCGISupervisor(helper.LocalWSGISupervisor): + + httpserver_class = "cherrypy.process.servers.FlupFCGIServer" + using_apache = True + using_wsgi = True + template = conf_fastcgi + + def __str__(self): + return "FCGI Server on %s:%s" % (self.host, self.port) + + def start(self, modulename): + cherrypy.server.httpserver = servers.FlupFCGIServer( + application=erase_script_name, bindAddress=('127.0.0.1', 4000)) + cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000) + cherrypy.server.socket_port = 4000 + # For FCGI, we both start apache... + self.start_apache() + # ...and our local server + cherrypy.engine.start() + self.sync_apps() + + def start_apache(self): + fcgiconf = CONF_PATH + if not os.path.isabs(fcgiconf): + fcgiconf = os.path.join(curdir, fcgiconf) + + # Write the Apache conf file. + f = open(fcgiconf, 'wb') + try: + server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1] + output = self.template % {'port': self.port, 'root': curdir, + 'server': server} + output = output.replace('\r\n', '\n') + f.write(output) + finally: + f.close() + + result = read_process(APACHE_PATH, "-k start -f %s" % fcgiconf) + if result: + print(result) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, "-k stop") + helper.LocalWSGISupervisor.stop(self) + + def sync_apps(self): + cherrypy.server.httpserver.fcgiserver.application = self.get_app(erase_script_name) + diff --git a/libs/cherrypy/test/modfcgid.py b/libs/cherrypy/test/modfcgid.py new file mode 100644 index 0000000..736aa4c --- /dev/null +++ b/libs/cherrypy/test/modfcgid.py @@ -0,0 +1,125 @@ +"""Wrapper for mod_fcgid, for use as a CherryPy HTTP server when testing. + +To autostart fcgid, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl", "apache2ctl", +or "httpd"--create a symlink to them if needed. + +You'll also need the WSGIServer from flup.servers. +See http://projects.amor.org/misc/wiki/ModPythonGateway + + +KNOWN BUGS +========== + +1. Apache processes Range headers automatically; CherryPy's truncated + output is then truncated again by Apache. See test_core.testRanges. + This was worked around in http://www.cherrypy.org/changeset/1319. +2. Apache does not allow custom HTTP methods like CONNECT as per the spec. + See test_core.testHTTPMethods. +3. Max request header and body settings do not work with Apache. +4. Apache replaces status "reason phrases" automatically. For example, + CherryPy may set "304 Not modified" but Apache will write out + "304 Not Modified" (capital "M"). +5. Apache does not allow custom error codes as per the spec. +6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the + Request-URI too early. +7. mod_python will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. Apache will output a "Content-Length: 0" response header even if there's + no response entity body. This isn't really a bug; it just differs from + the CherryPy default. +""" + +import os +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) +import re +import sys +import time + +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy.process import plugins, servers +from cherrypy.test import helper + + +def read_process(cmd, args=""): + pipein, pipeout = os.popen4("%s %s" % (cmd, args)) + try: + firstline = pipeout.readline() + if (re.search(r"(not recognized|No such file|not found)", firstline, + re.IGNORECASE)): + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +APACHE_PATH = "httpd" +CONF_PATH = "fcgi.conf" + +conf_fcgid = """ +# Apache2 server conf file for testing CherryPy with mod_fcgid. + +DocumentRoot "%(root)s" +ServerName 127.0.0.1 +Listen %(port)s +LoadModule fastcgi_module modules/mod_fastcgi.dll +LoadModule rewrite_module modules/mod_rewrite.so + +Options ExecCGI +SetHandler fastcgi-script +RewriteEngine On +RewriteRule ^(.*)$ /fastcgi.pyc [L] +FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000 +""" + +class ModFCGISupervisor(helper.LocalSupervisor): + + using_apache = True + using_wsgi = True + template = conf_fcgid + + def __str__(self): + return "FCGI Server on %s:%s" % (self.host, self.port) + + def start(self, modulename): + cherrypy.server.httpserver = servers.FlupFCGIServer( + application=cherrypy.tree, bindAddress=('127.0.0.1', 4000)) + cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000) + # For FCGI, we both start apache... + self.start_apache() + # ...and our local server + helper.LocalServer.start(self, modulename) + + def start_apache(self): + fcgiconf = CONF_PATH + if not os.path.isabs(fcgiconf): + fcgiconf = os.path.join(curdir, fcgiconf) + + # Write the Apache conf file. + f = open(fcgiconf, 'wb') + try: + server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1] + output = self.template % {'port': self.port, 'root': curdir, + 'server': server} + output = ntob(output.replace('\r\n', '\n')) + f.write(output) + finally: + f.close() + + result = read_process(APACHE_PATH, "-k start -f %s" % fcgiconf) + if result: + print(result) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, "-k stop") + helper.LocalServer.stop(self) + + def sync_apps(self): + cherrypy.server.httpserver.fcgiserver.application = self.get_app() + diff --git a/libs/cherrypy/test/modpy.py b/libs/cherrypy/test/modpy.py new file mode 100644 index 0000000..519571f --- /dev/null +++ b/libs/cherrypy/test/modpy.py @@ -0,0 +1,163 @@ +"""Wrapper for mod_python, for use as a CherryPy HTTP server when testing. + +To autostart modpython, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl" or "apache2ctl"-- +create a symlink to them if needed. + +If you wish to test the WSGI interface instead of our _cpmodpy interface, +you also need the 'modpython_gateway' module at: +http://projects.amor.org/misc/wiki/ModPythonGateway + + +KNOWN BUGS +========== + +1. Apache processes Range headers automatically; CherryPy's truncated + output is then truncated again by Apache. See test_core.testRanges. + This was worked around in http://www.cherrypy.org/changeset/1319. +2. Apache does not allow custom HTTP methods like CONNECT as per the spec. + See test_core.testHTTPMethods. +3. Max request header and body settings do not work with Apache. +4. Apache replaces status "reason phrases" automatically. For example, + CherryPy may set "304 Not modified" but Apache will write out + "304 Not Modified" (capital "M"). +5. Apache does not allow custom error codes as per the spec. +6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the + Request-URI too early. +7. mod_python will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. Apache will output a "Content-Length: 0" response header even if there's + no response entity body. This isn't really a bug; it just differs from + the CherryPy default. +""" + +import os +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) +import re +import time + +from cherrypy.test import helper + + +def read_process(cmd, args=""): + pipein, pipeout = os.popen4("%s %s" % (cmd, args)) + try: + firstline = pipeout.readline() + if (re.search(r"(not recognized|No such file|not found)", firstline, + re.IGNORECASE)): + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +APACHE_PATH = "httpd" +CONF_PATH = "test_mp.conf" + +conf_modpython_gateway = """ +# Apache2 server conf file for testing CherryPy with modpython_gateway. + +ServerName 127.0.0.1 +DocumentRoot "/" +Listen %(port)s +LoadModule python_module modules/mod_python.so + +SetHandler python-program +PythonFixupHandler cherrypy.test.modpy::wsgisetup +PythonOption testmod %(modulename)s +PythonHandler modpython_gateway::handler +PythonOption wsgi.application cherrypy::tree +PythonOption socket_host %(host)s +PythonDebug On +""" + +conf_cpmodpy = """ +# Apache2 server conf file for testing CherryPy with _cpmodpy. + +ServerName 127.0.0.1 +DocumentRoot "/" +Listen %(port)s +LoadModule python_module modules/mod_python.so + +SetHandler python-program +PythonFixupHandler cherrypy.test.modpy::cpmodpysetup +PythonHandler cherrypy._cpmodpy::handler +PythonOption cherrypy.setup cherrypy.test.%(modulename)s::setup_server +PythonOption socket_host %(host)s +PythonDebug On +""" + +class ModPythonSupervisor(helper.Supervisor): + + using_apache = True + using_wsgi = False + template = None + + def __str__(self): + return "ModPython Server on %s:%s" % (self.host, self.port) + + def start(self, modulename): + mpconf = CONF_PATH + if not os.path.isabs(mpconf): + mpconf = os.path.join(curdir, mpconf) + + f = open(mpconf, 'wb') + try: + f.write(self.template % + {'port': self.port, 'modulename': modulename, + 'host': self.host}) + finally: + f.close() + + result = read_process(APACHE_PATH, "-k start -f %s" % mpconf) + if result: + print(result) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, "-k stop") + + +loaded = False +def wsgisetup(req): + global loaded + if not loaded: + loaded = True + options = req.get_options() + + import cherrypy + cherrypy.config.update({ + "log.error_file": os.path.join(curdir, "test.log"), + "environment": "test_suite", + "server.socket_host": options['socket_host'], + }) + + modname = options['testmod'] + mod = __import__(modname, globals(), locals(), ['']) + mod.setup_server() + + cherrypy.server.unsubscribe() + cherrypy.engine.start() + from mod_python import apache + return apache.OK + + +def cpmodpysetup(req): + global loaded + if not loaded: + loaded = True + options = req.get_options() + + import cherrypy + cherrypy.config.update({ + "log.error_file": os.path.join(curdir, "test.log"), + "environment": "test_suite", + "server.socket_host": options['socket_host'], + }) + from mod_python import apache + return apache.OK + diff --git a/libs/cherrypy/test/modwsgi.py b/libs/cherrypy/test/modwsgi.py new file mode 100644 index 0000000..309a541 --- /dev/null +++ b/libs/cherrypy/test/modwsgi.py @@ -0,0 +1,148 @@ +"""Wrapper for mod_wsgi, for use as a CherryPy HTTP server. + +To autostart modwsgi, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl" or "apache2ctl"-- +create a symlink to them if needed. + + +KNOWN BUGS +========== + +##1. Apache processes Range headers automatically; CherryPy's truncated +## output is then truncated again by Apache. See test_core.testRanges. +## This was worked around in http://www.cherrypy.org/changeset/1319. +2. Apache does not allow custom HTTP methods like CONNECT as per the spec. + See test_core.testHTTPMethods. +3. Max request header and body settings do not work with Apache. +##4. Apache replaces status "reason phrases" automatically. For example, +## CherryPy may set "304 Not modified" but Apache will write out +## "304 Not Modified" (capital "M"). +##5. Apache does not allow custom error codes as per the spec. +##6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the +## Request-URI too early. +7. mod_wsgi will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. When responding with 204 No Content, mod_wsgi adds a Content-Length + header for you. +9. When an error is raised, mod_wsgi has no facility for printing a + traceback as the response content (it's sent to the Apache log instead). +10. Startup and shutdown of Apache when running mod_wsgi seems slow. +""" + +import os +curdir = os.path.abspath(os.path.dirname(__file__)) +import re +import sys +import time + +import cherrypy +from cherrypy.test import helper, webtest + + +def read_process(cmd, args=""): + pipein, pipeout = os.popen4("%s %s" % (cmd, args)) + try: + firstline = pipeout.readline() + if (re.search(r"(not recognized|No such file|not found)", firstline, + re.IGNORECASE)): + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +if sys.platform == 'win32': + APACHE_PATH = "httpd" +else: + APACHE_PATH = "apache" + +CONF_PATH = "test_mw.conf" + +conf_modwsgi = r""" +# Apache2 server conf file for testing CherryPy with modpython_gateway. + +ServerName 127.0.0.1 +DocumentRoot "/" +Listen %(port)s + +AllowEncodedSlashes On +LoadModule rewrite_module modules/mod_rewrite.so +RewriteEngine on +RewriteMap escaping int:escape + +LoadModule log_config_module modules/mod_log_config.so +LogFormat "%%h %%l %%u %%t \"%%r\" %%>s %%b \"%%{Referer}i\" \"%%{User-agent}i\"" combined +CustomLog "%(curdir)s/apache.access.log" combined +ErrorLog "%(curdir)s/apache.error.log" +LogLevel debug + +LoadModule wsgi_module modules/mod_wsgi.so +LoadModule env_module modules/mod_env.so + +WSGIScriptAlias / "%(curdir)s/modwsgi.py" +SetEnv testmod %(testmod)s +""" + + +class ModWSGISupervisor(helper.Supervisor): + """Server Controller for ModWSGI and CherryPy.""" + + using_apache = True + using_wsgi = True + template=conf_modwsgi + + def __str__(self): + return "ModWSGI Server on %s:%s" % (self.host, self.port) + + def start(self, modulename): + mpconf = CONF_PATH + if not os.path.isabs(mpconf): + mpconf = os.path.join(curdir, mpconf) + + f = open(mpconf, 'wb') + try: + output = (self.template % + {'port': self.port, 'testmod': modulename, + 'curdir': curdir}) + f.write(output) + finally: + f.close() + + result = read_process(APACHE_PATH, "-k start -f %s" % mpconf) + if result: + print(result) + + # Make a request so mod_wsgi starts up our app. + # If we don't, concurrent initial requests will 404. + cherrypy._cpserver.wait_for_occupied_port("127.0.0.1", self.port) + webtest.openURL('/ihopetheresnodefault', port=self.port) + time.sleep(1) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, "-k stop") + + +loaded = False +def application(environ, start_response): + import cherrypy + global loaded + if not loaded: + loaded = True + modname = "cherrypy.test." + environ['testmod'] + mod = __import__(modname, globals(), locals(), ['']) + mod.setup_server() + + cherrypy.config.update({ + "log.error_file": os.path.join(curdir, "test.error.log"), + "log.access_file": os.path.join(curdir, "test.access.log"), + "environment": "test_suite", + "engine.SIGHUP": None, + "engine.SIGTERM": None, + }) + return cherrypy.tree(environ, start_response) + diff --git a/libs/cherrypy/test/sessiondemo.py b/libs/cherrypy/test/sessiondemo.py new file mode 100644 index 0000000..342e5b5 --- /dev/null +++ b/libs/cherrypy/test/sessiondemo.py @@ -0,0 +1,153 @@ +#!/usr/bin/python +"""A session demonstration app.""" + +import calendar +from datetime import datetime +import sys +import cherrypy +from cherrypy.lib import sessions +from cherrypy._cpcompat import copyitems + + +page = """ + + + + + + + +

Session Demo

+

Reload this page. The session ID should not change from one reload to the next

+

Index | Expire | Regenerate

+ + + + + + + + + +
Session ID:%(sessionid)s

%(changemsg)s

Request Cookie%(reqcookie)s
Response Cookie%(respcookie)s

Session Data%(sessiondata)s
Server Time%(servertime)s (Unix time: %(serverunixtime)s)
Browser Time 
Cherrypy Version:%(cpversion)s
Python Version:%(pyversion)s
+ +""" + +class Root(object): + + def page(self): + changemsg = [] + if cherrypy.session.id != cherrypy.session.originalid: + if cherrypy.session.originalid is None: + changemsg.append('Created new session because no session id was given.') + if cherrypy.session.missing: + changemsg.append('Created new session due to missing (expired or malicious) session.') + if cherrypy.session.regenerated: + changemsg.append('Application generated a new session.') + + try: + expires = cherrypy.response.cookie['session_id']['expires'] + except KeyError: + expires = '' + + return page % { + 'sessionid': cherrypy.session.id, + 'changemsg': '
'.join(changemsg), + 'respcookie': cherrypy.response.cookie.output(), + 'reqcookie': cherrypy.request.cookie.output(), + 'sessiondata': copyitems(cherrypy.session), + 'servertime': datetime.utcnow().strftime("%Y/%m/%d %H:%M") + " UTC", + 'serverunixtime': calendar.timegm(datetime.utcnow().timetuple()), + 'cpversion': cherrypy.__version__, + 'pyversion': sys.version, + 'expires': expires, + } + + def index(self): + # Must modify data or the session will not be saved. + cherrypy.session['color'] = 'green' + return self.page() + index.exposed = True + + def expire(self): + sessions.expire() + return self.page() + expire.exposed = True + + def regen(self): + cherrypy.session.regenerate() + # Must modify data or the session will not be saved. + cherrypy.session['color'] = 'yellow' + return self.page() + regen.exposed = True + +if __name__ == '__main__': + cherrypy.config.update({ + #'environment': 'production', + 'log.screen': True, + 'tools.sessions.on': True, + }) + cherrypy.quickstart(Root()) + diff --git a/libs/cherrypy/test/static/dirback.jpg b/libs/cherrypy/test/static/dirback.jpg new file mode 100644 index 0000000000000000000000000000000000000000..530e6d6a386fc097f3a1dbabbde2d80fec1175ac GIT binary patch literal 18238 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�H;co1wWja&aY$n> z=Jt*bcyA#%hhcNqg668+&REt|@npB)Tns;=irh|HHDMT^HWE z#C$boDf4Wp?=c*f{G|rh8Ioq4D|YQ=dfFnlQvJ-`<-5)X^Uo-Y;(1l2Yn4*H<|~6K z-)Yv*>Wj`AH1MPxX55$d;&P9#wA%Nk9nPyH58q!E#vdl$LqWtF_L+4XMD z<`>%@IIkv%g^ORQb-`?yf6?-Sm$L9{tgD z`k9^E#p_?gKD6vyvg72GscOFuri))ZcHBO6X%fq6pE-Sd=5)1~9&AtioGo7L|6qym zX0b;LU6R=H6Mt)0=zgC%vr@aFf6BXV`%XjGW67x*ALSMAiscJ^peX%%j0+cL3whIiM} zHTTcVKeBb5(L;5vnaK$pn`+(k$~2ho|8&{;z3u#Wz2a!KTY3jB8YsQI7OU5|xlU*X zliOvFuin0E`6uYj@<|JF=lxV(I`xv)3?9%J?qtb%ukw?);-*}=dirYUP5F>^2+`5N%)27^VE=O3@H4k{i~jjr7~wMxWv`;CO_ zw!x;2ODqg0*$XlDOx`3sD@Wx`Ytm)*CYB=~rle1r9dY16L8nW@7GFcN0Mob_mo4#K hs~Z9j+;LCQxz+hB)cnY~*Aq-`s;892Y`FaYCIBsyhfx3k literal 0 HcmV?d00001 diff --git a/libs/cherrypy/test/static/index.html b/libs/cherrypy/test/static/index.html new file mode 100644 index 0000000..b9f5f09 --- /dev/null +++ b/libs/cherrypy/test/static/index.html @@ -0,0 +1 @@ +Hello, world diff --git a/libs/cherrypy/test/style.css b/libs/cherrypy/test/style.css new file mode 100644 index 0000000..b266e93 --- /dev/null +++ b/libs/cherrypy/test/style.css @@ -0,0 +1 @@ +Dummy stylesheet diff --git a/libs/cherrypy/test/test.pem b/libs/cherrypy/test/test.pem new file mode 100644 index 0000000..47a4704 --- /dev/null +++ b/libs/cherrypy/test/test.pem @@ -0,0 +1,38 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDBKo554mzIMY+AByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZ +R9L4WtImEew05FY3Izerfm3MN3+MC0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Kn +da+O6xldVSosu8Ev3z9VZ94iC/ZgKzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQAB +AoGAWOCF0ZrWxn3XMucWq2LNwPKqlvVGwbIwX3cDmX22zmnM4Fy6arXbYh4XlyCj +9+ofqRrxIFz5k/7tFriTmZ0xag5+Jdx+Kwg0/twiP7XCNKipFogwe1Hznw8OFAoT +enKBdj2+/n2o0Bvo/tDB59m9L/538d46JGQUmJlzMyqYikECQQDyoq+8CtMNvE18 +8VgHcR/KtApxWAjj4HpaHYL637ATjThetUZkW92mgDgowyplthusxdNqhHWyv7E8 +tWNdYErZAkEAy85ShTR0M5aWmrE7o0r0SpWInAkNBH9aXQRRARFYsdBtNfRu6I0i +0lvU9wiu3eF57FMEC86yViZ5UBnQfTu7vQJAVesj/Zt7pwaCDfdMa740OsxMUlyR +MVhhGx4OLpYdPJ8qUecxGQKq13XZ7R1HGyNEY4bd2X80Smq08UFuATfC6QJAH8UB +yBHtKz2GLIcELOg6PIYizW/7v3+6rlVF60yw7sb2vzpjL40QqIn4IKoR2DSVtOkb +8FtAIX3N21aq0VrGYQJBAIPiaEc2AZ8Bq2GC4F3wOz/BxJ/izvnkiotR12QK4fh5 +yjZMhTjWCas5zwHR5PDjlD88AWGDMsZ1PicD4348xJQ= +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAy6gAwIBAgIJAI18BD7eQxlGMA0GCSqGSIb3DQEBBAUAMIGeMQswCQYD +VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU2FuIERpZWdv +MRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0MREwDwYDVQQLEwhkZXYtdGVzdDEW +MBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4GCSqGSIb3DQEJARYRcmVtaUBjaGVy +cnlweS5vcmcwHhcNMDYwOTA5MTkyMDIwWhcNMzQwMTI0MTkyMDIwWjCBnjELMAkG +A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVNhbiBEaWVn +bzEZMBcGA1UEChMQQ2hlcnJ5UHkgUHJvamVjdDERMA8GA1UECxMIZGV2LXRlc3Qx +FjAUBgNVBAMTDUNoZXJyeVB5IFRlYW0xIDAeBgkqhkiG9w0BCQEWEXJlbWlAY2hl +cnJ5cHkub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBKo554mzIMY+A +ByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZR9L4WtImEew05FY3Izerfm3MN3+M +C0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Knda+O6xldVSosu8Ev3z9VZ94iC/Zg +KzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQABo4IBBzCCAQMwHQYDVR0OBBYEFDIQ +2feb71tVZCWpU0qJ/Tw+wdtoMIHTBgNVHSMEgcswgciAFDIQ2feb71tVZCWpU0qJ +/Tw+wdtooYGkpIGhMIGeMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p +YTESMBAGA1UEBxMJU2FuIERpZWdvMRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0 +MREwDwYDVQQLEwhkZXYtdGVzdDEWMBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4G +CSqGSIb3DQEJARYRcmVtaUBjaGVycnlweS5vcmeCCQCNfAQ+3kMZRjAMBgNVHRME +BTADAQH/MA0GCSqGSIb3DQEBBAUAA4GBAL7AAQz7IePV48ZTAFHKr88ntPALsL5S +8vHCZPNMevNkLTj3DYUw2BcnENxMjm1kou2F2BkvheBPNZKIhc6z4hAml3ed1xa2 +D7w6e6OTcstdK/+KrPDDHeOP1dhMWNs2JE1bNlfF1LiXzYKSXpe88eCKjCXsCT/T +NluCaWQys3MS +-----END CERTIFICATE----- diff --git a/libs/cherrypy/test/test_auth_basic.py b/libs/cherrypy/test/test_auth_basic.py new file mode 100644 index 0000000..3a9781d --- /dev/null +++ b/libs/cherrypy/test/test_auth_basic.py @@ -0,0 +1,79 @@ +# This file is part of CherryPy +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 + +import cherrypy +from cherrypy._cpcompat import md5, ntob +from cherrypy.lib import auth_basic +from cherrypy.test import helper + + +class BasicAuthTest(helper.CPWebCase): + + def setup_server(): + class Root: + def index(self): + return "This is public." + index.exposed = True + + class BasicProtected: + def index(self): + return "Hello %s, you've been authorized." % cherrypy.request.login + index.exposed = True + + class BasicProtected2: + def index(self): + return "Hello %s, you've been authorized." % cherrypy.request.login + index.exposed = True + + userpassdict = {'xuser' : 'xpassword'} + userhashdict = {'xuser' : md5(ntob('xpassword')).hexdigest()} + + def checkpasshash(realm, user, password): + p = userhashdict.get(user) + return p and p == md5(ntob(password)).hexdigest() or False + + conf = {'/basic': {'tools.auth_basic.on': True, + 'tools.auth_basic.realm': 'wonderland', + 'tools.auth_basic.checkpassword': auth_basic.checkpassword_dict(userpassdict)}, + '/basic2': {'tools.auth_basic.on': True, + 'tools.auth_basic.realm': 'wonderland', + 'tools.auth_basic.checkpassword': checkpasshash}, + } + + root = Root() + root.basic = BasicProtected() + root.basic2 = BasicProtected2() + cherrypy.tree.mount(root, config=conf) + setup_server = staticmethod(setup_server) + + def testPublic(self): + self.getPage("/") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.assertBody('This is public.') + + def testBasic(self): + self.getPage("/basic/") + self.assertStatus(401) + self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"') + + self.getPage('/basic/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) + self.assertStatus(401) + + self.getPage('/basic/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')]) + self.assertStatus('200 OK') + self.assertBody("Hello xuser, you've been authorized.") + + def testBasic2(self): + self.getPage("/basic2/") + self.assertStatus(401) + self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"') + + self.getPage('/basic2/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) + self.assertStatus(401) + + self.getPage('/basic2/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')]) + self.assertStatus('200 OK') + self.assertBody("Hello xuser, you've been authorized.") + diff --git a/libs/cherrypy/test/test_auth_digest.py b/libs/cherrypy/test/test_auth_digest.py new file mode 100644 index 0000000..1960fa8 --- /dev/null +++ b/libs/cherrypy/test/test_auth_digest.py @@ -0,0 +1,115 @@ +# This file is part of CherryPy +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 + + +import cherrypy +from cherrypy.lib import auth_digest + +from cherrypy.test import helper + +class DigestAuthTest(helper.CPWebCase): + + def setup_server(): + class Root: + def index(self): + return "This is public." + index.exposed = True + + class DigestProtected: + def index(self): + return "Hello %s, you've been authorized." % cherrypy.request.login + index.exposed = True + + def fetch_users(): + return {'test': 'test'} + + + get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(fetch_users()) + conf = {'/digest': {'tools.auth_digest.on': True, + 'tools.auth_digest.realm': 'localhost', + 'tools.auth_digest.get_ha1': get_ha1, + 'tools.auth_digest.key': 'a565c27146791cfb', + 'tools.auth_digest.debug': 'True'}} + + root = Root() + root.digest = DigestProtected() + cherrypy.tree.mount(root, config=conf) + setup_server = staticmethod(setup_server) + + def testPublic(self): + self.getPage("/") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.assertBody('This is public.') + + def testDigest(self): + self.getPage("/digest/") + self.assertStatus(401) + + value = None + for k, v in self.headers: + if k.lower() == "www-authenticate": + if v.startswith("Digest"): + value = v + break + + if value is None: + self._handlewebError("Digest authentification scheme was not found") + + value = value[7:] + items = value.split(', ') + tokens = {} + for item in items: + key, value = item.split('=') + tokens[key.lower()] = value + + missing_msg = "%s is missing" + bad_value_msg = "'%s' was expecting '%s' but found '%s'" + nonce = None + if 'realm' not in tokens: + self._handlewebError(missing_msg % 'realm') + elif tokens['realm'] != '"localhost"': + self._handlewebError(bad_value_msg % ('realm', '"localhost"', tokens['realm'])) + if 'nonce' not in tokens: + self._handlewebError(missing_msg % 'nonce') + else: + nonce = tokens['nonce'].strip('"') + if 'algorithm' not in tokens: + self._handlewebError(missing_msg % 'algorithm') + elif tokens['algorithm'] != '"MD5"': + self._handlewebError(bad_value_msg % ('algorithm', '"MD5"', tokens['algorithm'])) + if 'qop' not in tokens: + self._handlewebError(missing_msg % 'qop') + elif tokens['qop'] != '"auth"': + self._handlewebError(bad_value_msg % ('qop', '"auth"', tokens['qop'])) + + get_ha1 = auth_digest.get_ha1_dict_plain({'test' : 'test'}) + + # Test user agent response with a wrong value for 'realm' + base_auth = 'Digest username="test", realm="wrong realm", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"' + + auth_header = base_auth % (nonce, '11111111111111111111111111111111', '00000001') + auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET') + # calculate the response digest + ha1 = get_ha1(auth.realm, 'test') + response = auth.request_digest(ha1) + # send response with correct response digest, but wrong realm + auth_header = base_auth % (nonce, response, '00000001') + self.getPage('/digest/', [('Authorization', auth_header)]) + self.assertStatus(401) + + # Test that must pass + base_auth = 'Digest username="test", realm="localhost", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"' + + auth_header = base_auth % (nonce, '11111111111111111111111111111111', '00000001') + auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET') + # calculate the response digest + ha1 = get_ha1('localhost', 'test') + response = auth.request_digest(ha1) + # send response with correct response digest + auth_header = base_auth % (nonce, response, '00000001') + self.getPage('/digest/', [('Authorization', auth_header)]) + self.assertStatus('200 OK') + self.assertBody("Hello test, you've been authorized.") + diff --git a/libs/cherrypy/test/test_bus.py b/libs/cherrypy/test/test_bus.py new file mode 100644 index 0000000..51c1022 --- /dev/null +++ b/libs/cherrypy/test/test_bus.py @@ -0,0 +1,263 @@ +import threading +import time +import unittest + +import cherrypy +from cherrypy._cpcompat import get_daemon, set +from cherrypy.process import wspbus + + +msg = "Listener %d on channel %s: %s." + + +class PublishSubscribeTests(unittest.TestCase): + + def get_listener(self, channel, index): + def listener(arg=None): + self.responses.append(msg % (index, channel, arg)) + return listener + + def test_builtin_channels(self): + b = wspbus.Bus() + + self.responses, expected = [], [] + + for channel in b.listeners: + for index, priority in enumerate([100, 50, 0, 51]): + b.subscribe(channel, self.get_listener(channel, index), priority) + + for channel in b.listeners: + b.publish(channel) + expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)]) + b.publish(channel, arg=79347) + expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)]) + + self.assertEqual(self.responses, expected) + + def test_custom_channels(self): + b = wspbus.Bus() + + self.responses, expected = [], [] + + custom_listeners = ('hugh', 'louis', 'dewey') + for channel in custom_listeners: + for index, priority in enumerate([None, 10, 60, 40]): + b.subscribe(channel, self.get_listener(channel, index), priority) + + for channel in custom_listeners: + b.publish(channel, 'ah so') + expected.extend([msg % (i, channel, 'ah so') for i in (1, 3, 0, 2)]) + b.publish(channel) + expected.extend([msg % (i, channel, None) for i in (1, 3, 0, 2)]) + + self.assertEqual(self.responses, expected) + + def test_listener_errors(self): + b = wspbus.Bus() + + self.responses, expected = [], [] + channels = [c for c in b.listeners if c != 'log'] + + for channel in channels: + b.subscribe(channel, self.get_listener(channel, 1)) + # This will break since the lambda takes no args. + b.subscribe(channel, lambda: None, priority=20) + + for channel in channels: + self.assertRaises(wspbus.ChannelFailures, b.publish, channel, 123) + expected.append(msg % (1, channel, 123)) + + self.assertEqual(self.responses, expected) + + +class BusMethodTests(unittest.TestCase): + + def log(self, bus): + self._log_entries = [] + def logit(msg, level): + self._log_entries.append(msg) + bus.subscribe('log', logit) + + def assertLog(self, entries): + self.assertEqual(self._log_entries, entries) + + def get_listener(self, channel, index): + def listener(arg=None): + self.responses.append(msg % (index, channel, arg)) + return listener + + def test_start(self): + b = wspbus.Bus() + self.log(b) + + self.responses = [] + num = 3 + for index in range(num): + b.subscribe('start', self.get_listener('start', index)) + + b.start() + try: + # The start method MUST call all 'start' listeners. + self.assertEqual(set(self.responses), + set([msg % (i, 'start', None) for i in range(num)])) + # The start method MUST move the state to STARTED + # (or EXITING, if errors occur) + self.assertEqual(b.state, b.states.STARTED) + # The start method MUST log its states. + self.assertLog(['Bus STARTING', 'Bus STARTED']) + finally: + # Exit so the atexit handler doesn't complain. + b.exit() + + def test_stop(self): + b = wspbus.Bus() + self.log(b) + + self.responses = [] + num = 3 + for index in range(num): + b.subscribe('stop', self.get_listener('stop', index)) + + b.stop() + + # The stop method MUST call all 'stop' listeners. + self.assertEqual(set(self.responses), + set([msg % (i, 'stop', None) for i in range(num)])) + # The stop method MUST move the state to STOPPED + self.assertEqual(b.state, b.states.STOPPED) + # The stop method MUST log its states. + self.assertLog(['Bus STOPPING', 'Bus STOPPED']) + + def test_graceful(self): + b = wspbus.Bus() + self.log(b) + + self.responses = [] + num = 3 + for index in range(num): + b.subscribe('graceful', self.get_listener('graceful', index)) + + b.graceful() + + # The graceful method MUST call all 'graceful' listeners. + self.assertEqual(set(self.responses), + set([msg % (i, 'graceful', None) for i in range(num)])) + # The graceful method MUST log its states. + self.assertLog(['Bus graceful']) + + def test_exit(self): + b = wspbus.Bus() + self.log(b) + + self.responses = [] + num = 3 + for index in range(num): + b.subscribe('stop', self.get_listener('stop', index)) + b.subscribe('exit', self.get_listener('exit', index)) + + b.exit() + + # The exit method MUST call all 'stop' listeners, + # and then all 'exit' listeners. + self.assertEqual(set(self.responses), + set([msg % (i, 'stop', None) for i in range(num)] + + [msg % (i, 'exit', None) for i in range(num)])) + # The exit method MUST move the state to EXITING + self.assertEqual(b.state, b.states.EXITING) + # The exit method MUST log its states. + self.assertLog(['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED']) + + def test_wait(self): + b = wspbus.Bus() + + def f(method): + time.sleep(0.2) + getattr(b, method)() + + for method, states in [('start', [b.states.STARTED]), + ('stop', [b.states.STOPPED]), + ('start', [b.states.STARTING, b.states.STARTED]), + ('exit', [b.states.EXITING]), + ]: + threading.Thread(target=f, args=(method,)).start() + b.wait(states) + + # The wait method MUST wait for the given state(s). + if b.state not in states: + self.fail("State %r not in %r" % (b.state, states)) + + def test_block(self): + b = wspbus.Bus() + self.log(b) + + def f(): + time.sleep(0.2) + b.exit() + def g(): + time.sleep(0.4) + threading.Thread(target=f).start() + threading.Thread(target=g).start() + threads = [t for t in threading.enumerate() if not get_daemon(t)] + self.assertEqual(len(threads), 3) + + b.block() + + # The block method MUST wait for the EXITING state. + self.assertEqual(b.state, b.states.EXITING) + # The block method MUST wait for ALL non-main, non-daemon threads to finish. + threads = [t for t in threading.enumerate() if not get_daemon(t)] + self.assertEqual(len(threads), 1) + # The last message will mention an indeterminable thread name; ignore it + self.assertEqual(self._log_entries[:-1], + ['Bus STOPPING', 'Bus STOPPED', + 'Bus EXITING', 'Bus EXITED', + 'Waiting for child threads to terminate...']) + + def test_start_with_callback(self): + b = wspbus.Bus() + self.log(b) + try: + events = [] + def f(*args, **kwargs): + events.append(("f", args, kwargs)) + def g(): + events.append("g") + b.subscribe("start", g) + b.start_with_callback(f, (1, 3, 5), {"foo": "bar"}) + # Give wait() time to run f() + time.sleep(0.2) + + # The callback method MUST wait for the STARTED state. + self.assertEqual(b.state, b.states.STARTED) + # The callback method MUST run after all start methods. + self.assertEqual(events, ["g", ("f", (1, 3, 5), {"foo": "bar"})]) + finally: + b.exit() + + def test_log(self): + b = wspbus.Bus() + self.log(b) + self.assertLog([]) + + # Try a normal message. + expected = [] + for msg in ["O mah darlin'"] * 3 + ["Clementiiiiiiiine"]: + b.log(msg) + expected.append(msg) + self.assertLog(expected) + + # Try an error message + try: + foo + except NameError: + b.log("You are lost and gone forever", traceback=True) + lastmsg = self._log_entries[-1] + if "Traceback" not in lastmsg or "NameError" not in lastmsg: + self.fail("Last log message %r did not contain " + "the expected traceback." % lastmsg) + else: + self.fail("NameError was not raised as expected.") + + +if __name__ == "__main__": + unittest.main() diff --git a/libs/cherrypy/test/test_caching.py b/libs/cherrypy/test/test_caching.py new file mode 100644 index 0000000..c210e6e --- /dev/null +++ b/libs/cherrypy/test/test_caching.py @@ -0,0 +1,328 @@ +import datetime +import gzip +from itertools import count +import os +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) +import sys +import threading +import time +import urllib + +import cherrypy +from cherrypy._cpcompat import next, ntob, quote, xrange +from cherrypy.lib import httputil + +gif_bytes = ntob('GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00' + '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + '\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;') + + + +from cherrypy.test import helper + +class CacheTest(helper.CPWebCase): + + def setup_server(): + + class Root: + + _cp_config = {'tools.caching.on': True} + + def __init__(self): + self.counter = 0 + self.control_counter = 0 + self.longlock = threading.Lock() + + def index(self): + self.counter += 1 + msg = "visit #%s" % self.counter + return msg + index.exposed = True + + def control(self): + self.control_counter += 1 + return "visit #%s" % self.control_counter + control.exposed = True + + def a_gif(self): + cherrypy.response.headers['Last-Modified'] = httputil.HTTPDate() + return gif_bytes + a_gif.exposed = True + + def long_process(self, seconds='1'): + try: + self.longlock.acquire() + time.sleep(float(seconds)) + finally: + self.longlock.release() + return 'success!' + long_process.exposed = True + + def clear_cache(self, path): + cherrypy._cache.store[cherrypy.request.base + path].clear() + clear_cache.exposed = True + + class VaryHeaderCachingServer(object): + + _cp_config = {'tools.caching.on': True, + 'tools.response_headers.on': True, + 'tools.response_headers.headers': [('Vary', 'Our-Varying-Header')], + } + + def __init__(self): + self.counter = count(1) + + def index(self): + return "visit #%s" % next(self.counter) + index.exposed = True + + class UnCached(object): + _cp_config = {'tools.expires.on': True, + 'tools.expires.secs': 60, + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.root': curdir, + } + + def force(self): + cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' + self._cp_config['tools.expires.force'] = True + self._cp_config['tools.expires.secs'] = 0 + return "being forceful" + force.exposed = True + force._cp_config = {'tools.expires.secs': 0} + + def dynamic(self): + cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' + cherrypy.response.headers['Cache-Control'] = 'private' + return "D-d-d-dynamic!" + dynamic.exposed = True + + def cacheable(self): + cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' + return "Hi, I'm cacheable." + cacheable.exposed = True + + def specific(self): + cherrypy.response.headers['Etag'] = 'need_this_to_make_me_cacheable' + return "I am being specific" + specific.exposed = True + specific._cp_config = {'tools.expires.secs': 86400} + + class Foo(object):pass + + def wrongtype(self): + cherrypy.response.headers['Etag'] = 'need_this_to_make_me_cacheable' + return "Woops" + wrongtype.exposed = True + wrongtype._cp_config = {'tools.expires.secs': Foo()} + + cherrypy.tree.mount(Root()) + cherrypy.tree.mount(UnCached(), "/expires") + cherrypy.tree.mount(VaryHeaderCachingServer(), "/varying_headers") + cherrypy.config.update({'tools.gzip.on': True}) + setup_server = staticmethod(setup_server) + + def testCaching(self): + elapsed = 0.0 + for trial in range(10): + self.getPage("/") + # The response should be the same every time, + # except for the Age response header. + self.assertBody('visit #1') + if trial != 0: + age = int(self.assertHeader("Age")) + self.assert_(age >= elapsed) + elapsed = age + + # POST, PUT, DELETE should not be cached. + self.getPage("/", method="POST") + self.assertBody('visit #2') + # Because gzip is turned on, the Vary header should always Vary for content-encoding + self.assertHeader('Vary', 'Accept-Encoding') + # The previous request should have invalidated the cache, + # so this request will recalc the response. + self.getPage("/", method="GET") + self.assertBody('visit #3') + # ...but this request should get the cached copy. + self.getPage("/", method="GET") + self.assertBody('visit #3') + self.getPage("/", method="DELETE") + self.assertBody('visit #4') + + # The previous request should have invalidated the cache, + # so this request will recalc the response. + self.getPage("/", method="GET", headers=[('Accept-Encoding', 'gzip')]) + self.assertHeader('Content-Encoding', 'gzip') + self.assertHeader('Vary') + self.assertEqual(cherrypy.lib.encoding.decompress(self.body), ntob("visit #5")) + + # Now check that a second request gets the gzip header and gzipped body + # This also tests a bug in 3.0 to 3.0.2 whereby the cached, gzipped + # response body was being gzipped a second time. + self.getPage("/", method="GET", headers=[('Accept-Encoding', 'gzip')]) + self.assertHeader('Content-Encoding', 'gzip') + self.assertEqual(cherrypy.lib.encoding.decompress(self.body), ntob("visit #5")) + + # Now check that a third request that doesn't accept gzip + # skips the cache (because the 'Vary' header denies it). + self.getPage("/", method="GET") + self.assertNoHeader('Content-Encoding') + self.assertBody('visit #6') + + def testVaryHeader(self): + self.getPage("/varying_headers/") + self.assertStatus("200 OK") + self.assertHeaderItemValue('Vary', 'Our-Varying-Header') + self.assertBody('visit #1') + + # Now check that different 'Vary'-fields don't evict each other. + # This test creates 2 requests with different 'Our-Varying-Header' + # and then tests if the first one still exists. + self.getPage("/varying_headers/", headers=[('Our-Varying-Header', 'request 2')]) + self.assertStatus("200 OK") + self.assertBody('visit #2') + + self.getPage("/varying_headers/", headers=[('Our-Varying-Header', 'request 2')]) + self.assertStatus("200 OK") + self.assertBody('visit #2') + + self.getPage("/varying_headers/") + self.assertStatus("200 OK") + self.assertBody('visit #1') + + def testExpiresTool(self): + # test setting an expires header + self.getPage("/expires/specific") + self.assertStatus("200 OK") + self.assertHeader("Expires") + + # test exceptions for bad time values + self.getPage("/expires/wrongtype") + self.assertStatus(500) + self.assertInBody("TypeError") + + # static content should not have "cache prevention" headers + self.getPage("/expires/index.html") + self.assertStatus("200 OK") + self.assertNoHeader("Pragma") + self.assertNoHeader("Cache-Control") + self.assertHeader("Expires") + + # dynamic content that sets indicators should not have + # "cache prevention" headers + self.getPage("/expires/cacheable") + self.assertStatus("200 OK") + self.assertNoHeader("Pragma") + self.assertNoHeader("Cache-Control") + self.assertHeader("Expires") + + self.getPage('/expires/dynamic') + self.assertBody("D-d-d-dynamic!") + # the Cache-Control header should be untouched + self.assertHeader("Cache-Control", "private") + self.assertHeader("Expires") + + # configure the tool to ignore indicators and replace existing headers + self.getPage("/expires/force") + self.assertStatus("200 OK") + # This also gives us a chance to test 0 expiry with no other headers + self.assertHeader("Pragma", "no-cache") + if cherrypy.server.protocol_version == "HTTP/1.1": + self.assertHeader("Cache-Control", "no-cache, must-revalidate") + self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT") + + # static content should now have "cache prevention" headers + self.getPage("/expires/index.html") + self.assertStatus("200 OK") + self.assertHeader("Pragma", "no-cache") + if cherrypy.server.protocol_version == "HTTP/1.1": + self.assertHeader("Cache-Control", "no-cache, must-revalidate") + self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT") + + # the cacheable handler should now have "cache prevention" headers + self.getPage("/expires/cacheable") + self.assertStatus("200 OK") + self.assertHeader("Pragma", "no-cache") + if cherrypy.server.protocol_version == "HTTP/1.1": + self.assertHeader("Cache-Control", "no-cache, must-revalidate") + self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT") + + self.getPage('/expires/dynamic') + self.assertBody("D-d-d-dynamic!") + # dynamic sets Cache-Control to private but it should be + # overwritten here ... + self.assertHeader("Pragma", "no-cache") + if cherrypy.server.protocol_version == "HTTP/1.1": + self.assertHeader("Cache-Control", "no-cache, must-revalidate") + self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT") + + def testLastModified(self): + self.getPage("/a.gif") + self.assertStatus(200) + self.assertBody(gif_bytes) + lm1 = self.assertHeader("Last-Modified") + + # this request should get the cached copy. + self.getPage("/a.gif") + self.assertStatus(200) + self.assertBody(gif_bytes) + self.assertHeader("Age") + lm2 = self.assertHeader("Last-Modified") + self.assertEqual(lm1, lm2) + + # this request should match the cached copy, but raise 304. + self.getPage("/a.gif", [('If-Modified-Since', lm1)]) + self.assertStatus(304) + self.assertNoHeader("Last-Modified") + if not getattr(cherrypy.server, "using_apache", False): + self.assertHeader("Age") + + def test_antistampede(self): + SECONDS = 4 + # We MUST make an initial synchronous request in order to create the + # AntiStampedeCache object, and populate its selecting_headers, + # before the actual stampede. + self.getPage("/long_process?seconds=%d" % SECONDS) + self.assertBody('success!') + self.getPage("/clear_cache?path=" + + quote('/long_process?seconds=%d' % SECONDS, safe='')) + self.assertStatus(200) + + start = datetime.datetime.now() + def run(): + self.getPage("/long_process?seconds=%d" % SECONDS) + # The response should be the same every time + self.assertBody('success!') + ts = [threading.Thread(target=run) for i in xrange(100)] + for t in ts: + t.start() + for t in ts: + t.join() + self.assertEqualDates(start, datetime.datetime.now(), + # Allow a second (two, for slow hosts) + # for our thread/TCP overhead etc. + seconds=SECONDS + 2) + + def test_cache_control(self): + self.getPage("/control") + self.assertBody('visit #1') + self.getPage("/control") + self.assertBody('visit #1') + + self.getPage("/control", headers=[('Cache-Control', 'no-cache')]) + self.assertBody('visit #2') + self.getPage("/control") + self.assertBody('visit #2') + + self.getPage("/control", headers=[('Pragma', 'no-cache')]) + self.assertBody('visit #3') + self.getPage("/control") + self.assertBody('visit #3') + + time.sleep(1) + self.getPage("/control", headers=[('Cache-Control', 'max-age=0')]) + self.assertBody('visit #4') + self.getPage("/control") + self.assertBody('visit #4') + diff --git a/libs/cherrypy/test/test_config.py b/libs/cherrypy/test/test_config.py new file mode 100644 index 0000000..b1ef6a3 --- /dev/null +++ b/libs/cherrypy/test/test_config.py @@ -0,0 +1,256 @@ +"""Tests for the CherryPy configuration system.""" + +import os, sys +localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + +from cherrypy._cpcompat import ntob, StringIO +import unittest + +import cherrypy + +def setup_server(): + + class Root: + + _cp_config = {'foo': 'this', + 'bar': 'that'} + + def __init__(self): + cherrypy.config.namespaces['db'] = self.db_namespace + + def db_namespace(self, k, v): + if k == "scheme": + self.db = v + + # @cherrypy.expose(alias=('global_', 'xyz')) + def index(self, key): + return cherrypy.request.config.get(key, "None") + index = cherrypy.expose(index, alias=('global_', 'xyz')) + + def repr(self, key): + return repr(cherrypy.request.config.get(key, None)) + repr.exposed = True + + def dbscheme(self): + return self.db + dbscheme.exposed = True + + def plain(self, x): + return x + plain.exposed = True + plain._cp_config = {'request.body.attempt_charsets': ['utf-16']} + + favicon_ico = cherrypy.tools.staticfile.handler( + filename=os.path.join(localDir, '../favicon.ico')) + + class Foo: + + _cp_config = {'foo': 'this2', + 'baz': 'that2'} + + def index(self, key): + return cherrypy.request.config.get(key, "None") + index.exposed = True + nex = index + + def silly(self): + return 'Hello world' + silly.exposed = True + silly._cp_config = {'response.headers.X-silly': 'sillyval'} + + # Test the expose and config decorators + #@cherrypy.expose + #@cherrypy.config(foo='this3', **{'bax': 'this4'}) + def bar(self, key): + return repr(cherrypy.request.config.get(key, None)) + bar.exposed = True + bar._cp_config = {'foo': 'this3', 'bax': 'this4'} + + class Another: + + def index(self, key): + return str(cherrypy.request.config.get(key, "None")) + index.exposed = True + + + def raw_namespace(key, value): + if key == 'input.map': + handler = cherrypy.request.handler + def wrapper(): + params = cherrypy.request.params + for name, coercer in list(value.items()): + try: + params[name] = coercer(params[name]) + except KeyError: + pass + return handler() + cherrypy.request.handler = wrapper + elif key == 'output': + handler = cherrypy.request.handler + def wrapper(): + # 'value' is a type (like int or str). + return value(handler()) + cherrypy.request.handler = wrapper + + class Raw: + + _cp_config = {'raw.output': repr} + + def incr(self, num): + return num + 1 + incr.exposed = True + incr._cp_config = {'raw.input.map': {'num': int}} + + ioconf = StringIO(""" +[/] +neg: -1234 +filename: os.path.join(sys.prefix, "hello.py") +thing1: cherrypy.lib.httputil.response_codes[404] +thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2 +complex: 3+2j +mul: 6*3 +ones: "11" +twos: "22" +stradd: %%(ones)s + %%(twos)s + "33" + +[/favicon.ico] +tools.staticfile.filename = %r +""" % os.path.join(localDir, 'static/dirback.jpg')) + + root = Root() + root.foo = Foo() + root.raw = Raw() + app = cherrypy.tree.mount(root, config=ioconf) + app.request_class.namespaces['raw'] = raw_namespace + + cherrypy.tree.mount(Another(), "/another") + cherrypy.config.update({'luxuryyacht': 'throatwobblermangrove', + 'db.scheme': r"sqlite///memory", + }) + + +# Client-side code # + +from cherrypy.test import helper + +class ConfigTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def testConfig(self): + tests = [ + ('/', 'nex', 'None'), + ('/', 'foo', 'this'), + ('/', 'bar', 'that'), + ('/xyz', 'foo', 'this'), + ('/foo/', 'foo', 'this2'), + ('/foo/', 'bar', 'that'), + ('/foo/', 'bax', 'None'), + ('/foo/bar', 'baz', "'that2'"), + ('/foo/nex', 'baz', 'that2'), + # If 'foo' == 'this', then the mount point '/another' leaks into '/'. + ('/another/','foo', 'None'), + ] + for path, key, expected in tests: + self.getPage(path + "?key=" + key) + self.assertBody(expected) + + expectedconf = { + # From CP defaults + 'tools.log_headers.on': False, + 'tools.log_tracebacks.on': True, + 'request.show_tracebacks': True, + 'log.screen': False, + 'environment': 'test_suite', + 'engine.autoreload_on': False, + # From global config + 'luxuryyacht': 'throatwobblermangrove', + # From Root._cp_config + 'bar': 'that', + # From Foo._cp_config + 'baz': 'that2', + # From Foo.bar._cp_config + 'foo': 'this3', + 'bax': 'this4', + } + for key, expected in expectedconf.items(): + self.getPage("/foo/bar?key=" + key) + self.assertBody(repr(expected)) + + def testUnrepr(self): + self.getPage("/repr?key=neg") + self.assertBody("-1234") + + self.getPage("/repr?key=filename") + self.assertBody(repr(os.path.join(sys.prefix, "hello.py"))) + + self.getPage("/repr?key=thing1") + self.assertBody(repr(cherrypy.lib.httputil.response_codes[404])) + + if not getattr(cherrypy.server, "using_apache", False): + # The object ID's won't match up when using Apache, since the + # server and client are running in different processes. + self.getPage("/repr?key=thing2") + from cherrypy.tutorial import thing2 + self.assertBody(repr(thing2)) + + self.getPage("/repr?key=complex") + self.assertBody("(3+2j)") + + self.getPage("/repr?key=mul") + self.assertBody("18") + + self.getPage("/repr?key=stradd") + self.assertBody(repr("112233")) + + def testRespNamespaces(self): + self.getPage("/foo/silly") + self.assertHeader('X-silly', 'sillyval') + self.assertBody('Hello world') + + def testCustomNamespaces(self): + self.getPage("/raw/incr?num=12") + self.assertBody("13") + + self.getPage("/dbscheme") + self.assertBody(r"sqlite///memory") + + def testHandlerToolConfigOverride(self): + # Assert that config overrides tool constructor args. Above, we set + # the favicon in the page handler to be '../favicon.ico', + # but then overrode it in config to be './static/dirback.jpg'. + self.getPage("/favicon.ico") + self.assertBody(open(os.path.join(localDir, "static/dirback.jpg"), + "rb").read()) + + def test_request_body_namespace(self): + self.getPage("/plain", method='POST', headers=[ + ('Content-Type', 'application/x-www-form-urlencoded'), + ('Content-Length', '13')], + body=ntob('\xff\xfex\x00=\xff\xfea\x00b\x00c\x00')) + self.assertBody("abc") + + +class VariableSubstitutionTests(unittest.TestCase): + setup_server = staticmethod(setup_server) + + def test_config(self): + from textwrap import dedent + + # variable substitution with [DEFAULT] + conf = dedent(""" + [DEFAULT] + dir = "/some/dir" + my.dir = %(dir)s + "/sub" + + [my] + my.dir = %(dir)s + "/my/dir" + my.dir2 = %(my.dir)s + '/dir2' + + """) + + fp = StringIO(conf) + + cherrypy.config.update(fp) + self.assertEqual(cherrypy.config["my"]["my.dir"], "/some/dir/my/dir") + self.assertEqual(cherrypy.config["my"]["my.dir2"], "/some/dir/my/dir/dir2") + diff --git a/libs/cherrypy/test/test_config_server.py b/libs/cherrypy/test/test_config_server.py new file mode 100644 index 0000000..0b9718d --- /dev/null +++ b/libs/cherrypy/test/test_config_server.py @@ -0,0 +1,121 @@ +"""Tests for the CherryPy configuration system.""" + +import os, sys +localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) +import socket +import time + +import cherrypy + + +# Client-side code # + +from cherrypy.test import helper + +class ServerConfigTests(helper.CPWebCase): + + def setup_server(): + + class Root: + def index(self): + return cherrypy.request.wsgi_environ['SERVER_PORT'] + index.exposed = True + + def upload(self, file): + return "Size: %s" % len(file.file.read()) + upload.exposed = True + + def tinyupload(self): + return cherrypy.request.body.read() + tinyupload.exposed = True + tinyupload._cp_config = {'request.body.maxbytes': 100} + + cherrypy.tree.mount(Root()) + + cherrypy.config.update({ + 'server.socket_host': '0.0.0.0', + 'server.socket_port': 9876, + 'server.max_request_body_size': 200, + 'server.max_request_header_size': 500, + 'server.socket_timeout': 0.5, + + # Test explicit server.instance + 'server.2.instance': 'cherrypy._cpwsgi_server.CPWSGIServer', + 'server.2.socket_port': 9877, + + # Test non-numeric + # Also test default server.instance = builtin server + 'server.yetanother.socket_port': 9878, + }) + setup_server = staticmethod(setup_server) + + PORT = 9876 + + def testBasicConfig(self): + self.getPage("/") + self.assertBody(str(self.PORT)) + + def testAdditionalServers(self): + if self.scheme == 'https': + return self.skip("not available under ssl") + self.PORT = 9877 + self.getPage("/") + self.assertBody(str(self.PORT)) + self.PORT = 9878 + self.getPage("/") + self.assertBody(str(self.PORT)) + + def testMaxRequestSizePerHandler(self): + if getattr(cherrypy.server, "using_apache", False): + return self.skip("skipped due to known Apache differences... ") + + self.getPage('/tinyupload', method="POST", + headers=[('Content-Type', 'text/plain'), + ('Content-Length', '100')], + body="x" * 100) + self.assertStatus(200) + self.assertBody("x" * 100) + + self.getPage('/tinyupload', method="POST", + headers=[('Content-Type', 'text/plain'), + ('Content-Length', '101')], + body="x" * 101) + self.assertStatus(413) + + def testMaxRequestSize(self): + if getattr(cherrypy.server, "using_apache", False): + return self.skip("skipped due to known Apache differences... ") + + for size in (500, 5000, 50000): + self.getPage("/", headers=[('From', "x" * 500)]) + self.assertStatus(413) + + # Test for http://www.cherrypy.org/ticket/421 + # (Incorrect border condition in readline of SizeCheckWrapper). + # This hangs in rev 891 and earlier. + lines256 = "x" * 248 + self.getPage("/", + headers=[('Host', '%s:%s' % (self.HOST, self.PORT)), + ('From', lines256)]) + + # Test upload + body = '\r\n'.join([ + '--x', + 'Content-Disposition: form-data; name="file"; filename="hello.txt"', + 'Content-Type: text/plain', + '', + '%s', + '--x--']) + partlen = 200 - len(body) + b = body % ("x" * partlen) + h = [("Content-type", "multipart/form-data; boundary=x"), + ("Content-Length", "%s" % len(b))] + self.getPage('/upload', h, "POST", b) + self.assertBody('Size: %d' % partlen) + + b = body % ("x" * 200) + h = [("Content-type", "multipart/form-data; boundary=x"), + ("Content-Length", "%s" % len(b))] + self.getPage('/upload', h, "POST", b) + self.assertStatus(413) + diff --git a/libs/cherrypy/test/test_conn.py b/libs/cherrypy/test/test_conn.py new file mode 100644 index 0000000..1346f59 --- /dev/null +++ b/libs/cherrypy/test/test_conn.py @@ -0,0 +1,734 @@ +"""Tests for TCP connection handling, including proper and timely close.""" + +import socket +import sys +import time +timeout = 1 + + +import cherrypy +from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, NotConnected, BadStatusLine +from cherrypy._cpcompat import ntob, urlopen, unicodestr +from cherrypy.test import webtest +from cherrypy import _cperror + + +pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN' + +def setup_server(): + + def raise500(): + raise cherrypy.HTTPError(500) + + class Root: + + def index(self): + return pov + index.exposed = True + page1 = index + page2 = index + page3 = index + + def hello(self): + return "Hello, world!" + hello.exposed = True + + def timeout(self, t): + return str(cherrypy.server.httpserver.timeout) + timeout.exposed = True + + def stream(self, set_cl=False): + if set_cl: + cherrypy.response.headers['Content-Length'] = 10 + + def content(): + for x in range(10): + yield str(x) + + return content() + stream.exposed = True + stream._cp_config = {'response.stream': True} + + def error(self, code=500): + raise cherrypy.HTTPError(code) + error.exposed = True + + def upload(self): + if not cherrypy.request.method == 'POST': + raise AssertionError("'POST' != request.method %r" % + cherrypy.request.method) + return "thanks for '%s'" % cherrypy.request.body.read() + upload.exposed = True + + def custom(self, response_code): + cherrypy.response.status = response_code + return "Code = %s" % response_code + custom.exposed = True + + def err_before_read(self): + return "ok" + err_before_read.exposed = True + err_before_read._cp_config = {'hooks.on_start_resource': raise500} + + def one_megabyte_of_a(self): + return ["a" * 1024] * 1024 + one_megabyte_of_a.exposed = True + + def custom_cl(self, body, cl): + cherrypy.response.headers['Content-Length'] = cl + if not isinstance(body, list): + body = [body] + newbody = [] + for chunk in body: + if isinstance(chunk, unicodestr): + chunk = chunk.encode('ISO-8859-1') + newbody.append(chunk) + return newbody + custom_cl.exposed = True + # Turn off the encoding tool so it doens't collapse + # our response body and reclaculate the Content-Length. + custom_cl._cp_config = {'tools.encode.on': False} + + cherrypy.tree.mount(Root()) + cherrypy.config.update({ + 'server.max_request_body_size': 1001, + 'server.socket_timeout': timeout, + }) + + +from cherrypy.test import helper + +class ConnectionCloseTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_HTTP11(self): + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + self.persistent = True + + # Make the first request and assert there's no "Connection: close". + self.getPage("/") + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertNoHeader("Connection") + + # Make another request on the same connection. + self.getPage("/page1") + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertNoHeader("Connection") + + # Test client-side close. + self.getPage("/page2", headers=[("Connection", "close")]) + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertHeader("Connection", "close") + + # Make another request on the same connection, which should error. + self.assertRaises(NotConnected, self.getPage, "/") + + def test_Streaming_no_len(self): + self._streaming(set_cl=False) + + def test_Streaming_with_len(self): + self._streaming(set_cl=True) + + def _streaming(self, set_cl): + if cherrypy.server.protocol_version == "HTTP/1.1": + self.PROTOCOL = "HTTP/1.1" + + self.persistent = True + + # Make the first request and assert there's no "Connection: close". + self.getPage("/") + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertNoHeader("Connection") + + # Make another, streamed request on the same connection. + if set_cl: + # When a Content-Length is provided, the content should stream + # without closing the connection. + self.getPage("/stream?set_cl=Yes") + self.assertHeader("Content-Length") + self.assertNoHeader("Connection", "close") + self.assertNoHeader("Transfer-Encoding") + + self.assertStatus('200 OK') + self.assertBody('0123456789') + else: + # When no Content-Length response header is provided, + # streamed output will either close the connection, or use + # chunked encoding, to determine transfer-length. + self.getPage("/stream") + self.assertNoHeader("Content-Length") + self.assertStatus('200 OK') + self.assertBody('0123456789') + + chunked_response = False + for k, v in self.headers: + if k.lower() == "transfer-encoding": + if str(v) == "chunked": + chunked_response = True + + if chunked_response: + self.assertNoHeader("Connection", "close") + else: + self.assertHeader("Connection", "close") + + # Make another request on the same connection, which should error. + self.assertRaises(NotConnected, self.getPage, "/") + + # Try HEAD. See http://www.cherrypy.org/ticket/864. + self.getPage("/stream", method='HEAD') + self.assertStatus('200 OK') + self.assertBody('') + self.assertNoHeader("Transfer-Encoding") + else: + self.PROTOCOL = "HTTP/1.0" + + self.persistent = True + + # Make the first request and assert Keep-Alive. + self.getPage("/", headers=[("Connection", "Keep-Alive")]) + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertHeader("Connection", "Keep-Alive") + + # Make another, streamed request on the same connection. + if set_cl: + # When a Content-Length is provided, the content should + # stream without closing the connection. + self.getPage("/stream?set_cl=Yes", + headers=[("Connection", "Keep-Alive")]) + self.assertHeader("Content-Length") + self.assertHeader("Connection", "Keep-Alive") + self.assertNoHeader("Transfer-Encoding") + self.assertStatus('200 OK') + self.assertBody('0123456789') + else: + # When a Content-Length is not provided, + # the server should close the connection. + self.getPage("/stream", headers=[("Connection", "Keep-Alive")]) + self.assertStatus('200 OK') + self.assertBody('0123456789') + + self.assertNoHeader("Content-Length") + self.assertNoHeader("Connection", "Keep-Alive") + self.assertNoHeader("Transfer-Encoding") + + # Make another request on the same connection, which should error. + self.assertRaises(NotConnected, self.getPage, "/") + + def test_HTTP10_KeepAlive(self): + self.PROTOCOL = "HTTP/1.0" + if self.scheme == "https": + self.HTTP_CONN = HTTPSConnection + else: + self.HTTP_CONN = HTTPConnection + + # Test a normal HTTP/1.0 request. + self.getPage("/page2") + self.assertStatus('200 OK') + self.assertBody(pov) + # Apache, for example, may emit a Connection header even for HTTP/1.0 +## self.assertNoHeader("Connection") + + # Test a keep-alive HTTP/1.0 request. + self.persistent = True + + self.getPage("/page3", headers=[("Connection", "Keep-Alive")]) + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertHeader("Connection", "Keep-Alive") + + # Remove the keep-alive header again. + self.getPage("/page3") + self.assertStatus('200 OK') + self.assertBody(pov) + # Apache, for example, may emit a Connection header even for HTTP/1.0 +## self.assertNoHeader("Connection") + + +class PipelineTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_HTTP11_Timeout(self): + # If we timeout without sending any data, + # the server will close the conn with a 408. + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + # Connect but send nothing. + self.persistent = True + conn = self.HTTP_CONN + conn.auto_open = False + conn.connect() + + # Wait for our socket timeout + time.sleep(timeout * 2) + + # The request should have returned 408 already. + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.assertEqual(response.status, 408) + conn.close() + + # Connect but send half the headers only. + self.persistent = True + conn = self.HTTP_CONN + conn.auto_open = False + conn.connect() + conn.send(ntob('GET /hello HTTP/1.1')) + conn.send(("Host: %s" % self.HOST).encode('ascii')) + + # Wait for our socket timeout + time.sleep(timeout * 2) + + # The conn should have already sent 408. + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.assertEqual(response.status, 408) + conn.close() + + def test_HTTP11_Timeout_after_request(self): + # If we timeout after at least one request has succeeded, + # the server will close the conn without 408. + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + # Make an initial request + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest("GET", "/timeout?t=%s" % timeout, skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(str(timeout)) + + # Make a second request on the same socket + conn._output(ntob('GET /hello HTTP/1.1')) + conn._output(ntob("Host: %s" % self.HOST, 'ascii')) + conn._send_output() + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody("Hello, world!") + + # Wait for our socket timeout + time.sleep(timeout * 2) + + # Make another request on the same socket, which should error + conn._output(ntob('GET /hello HTTP/1.1')) + conn._output(ntob("Host: %s" % self.HOST, 'ascii')) + conn._send_output() + response = conn.response_class(conn.sock, method="GET") + try: + response.begin() + except: + if not isinstance(sys.exc_info()[1], + (socket.error, BadStatusLine)): + self.fail("Writing to timed out socket didn't fail" + " as it should have: %s" % sys.exc_info()[1]) + else: + if response.status != 408: + self.fail("Writing to timed out socket didn't fail" + " as it should have: %s" % + response.read()) + + conn.close() + + # Make another request on a new socket, which should work + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest("GET", "/", skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(pov) + + + # Make another request on the same socket, + # but timeout on the headers + conn.send(ntob('GET /hello HTTP/1.1')) + # Wait for our socket timeout + time.sleep(timeout * 2) + response = conn.response_class(conn.sock, method="GET") + try: + response.begin() + except: + if not isinstance(sys.exc_info()[1], + (socket.error, BadStatusLine)): + self.fail("Writing to timed out socket didn't fail" + " as it should have: %s" % sys.exc_info()[1]) + else: + self.fail("Writing to timed out socket didn't fail" + " as it should have: %s" % + response.read()) + + conn.close() + + # Retry the request on a new connection, which should work + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest("GET", "/", skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(pov) + conn.close() + + def test_HTTP11_pipelining(self): + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + # Test pipelining. httplib doesn't support this directly. + self.persistent = True + conn = self.HTTP_CONN + + # Put request 1 + conn.putrequest("GET", "/hello", skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + + for trial in range(5): + # Put next request + conn._output(ntob('GET /hello HTTP/1.1')) + conn._output(ntob("Host: %s" % self.HOST, 'ascii')) + conn._send_output() + + # Retrieve previous response + response = conn.response_class(conn.sock, method="GET") + response.begin() + body = response.read(13) + self.assertEqual(response.status, 200) + self.assertEqual(body, ntob("Hello, world!")) + + # Retrieve final response + response = conn.response_class(conn.sock, method="GET") + response.begin() + body = response.read() + self.assertEqual(response.status, 200) + self.assertEqual(body, ntob("Hello, world!")) + + conn.close() + + def test_100_Continue(self): + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + self.persistent = True + conn = self.HTTP_CONN + + # Try a page without an Expect request header first. + # Note that httplib's response.begin automatically ignores + # 100 Continue responses, so we must manually check for it. + conn.putrequest("POST", "/upload", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Content-Type", "text/plain") + conn.putheader("Content-Length", "4") + conn.endheaders() + conn.send(ntob("d'oh")) + response = conn.response_class(conn.sock, method="POST") + version, status, reason = response._read_status() + self.assertNotEqual(status, 100) + conn.close() + + # Now try a page with an Expect header... + conn.connect() + conn.putrequest("POST", "/upload", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Content-Type", "text/plain") + conn.putheader("Content-Length", "17") + conn.putheader("Expect", "100-continue") + conn.endheaders() + response = conn.response_class(conn.sock, method="POST") + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + self.assertEqual(status, 100) + while True: + line = response.fp.readline().strip() + if line: + self.fail("100 Continue should not output any headers. Got %r" % line) + else: + break + + # ...send the body + body = ntob("I am a small file") + conn.send(body) + + # ...get the final response + response.begin() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(200) + self.assertBody("thanks for '%s'" % body) + conn.close() + + +class ConnectionTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_readall_or_close(self): + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + if self.scheme == "https": + self.HTTP_CONN = HTTPSConnection + else: + self.HTTP_CONN = HTTPConnection + + # Test a max of 0 (the default) and then reset to what it was above. + old_max = cherrypy.server.max_request_body_size + for new_max in (0, old_max): + cherrypy.server.max_request_body_size = new_max + + self.persistent = True + conn = self.HTTP_CONN + + # Get a POST page with an error + conn.putrequest("POST", "/err_before_read", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Content-Type", "text/plain") + conn.putheader("Content-Length", "1000") + conn.putheader("Expect", "100-continue") + conn.endheaders() + response = conn.response_class(conn.sock, method="POST") + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + self.assertEqual(status, 100) + while True: + skip = response.fp.readline().strip() + if not skip: + break + + # ...send the body + conn.send(ntob("x" * 1000)) + + # ...get the final response + response.begin() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(500) + + # Now try a working page with an Expect header... + conn._output(ntob('POST /upload HTTP/1.1')) + conn._output(ntob("Host: %s" % self.HOST, 'ascii')) + conn._output(ntob("Content-Type: text/plain")) + conn._output(ntob("Content-Length: 17")) + conn._output(ntob("Expect: 100-continue")) + conn._send_output() + response = conn.response_class(conn.sock, method="POST") + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + self.assertEqual(status, 100) + while True: + skip = response.fp.readline().strip() + if not skip: + break + + # ...send the body + body = ntob("I am a small file") + conn.send(body) + + # ...get the final response + response.begin() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(200) + self.assertBody("thanks for '%s'" % body) + conn.close() + + def test_No_Message_Body(self): + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + # Set our HTTP_CONN to an instance so it persists between requests. + self.persistent = True + + # Make the first request and assert there's no "Connection: close". + self.getPage("/") + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertNoHeader("Connection") + + # Make a 204 request on the same connection. + self.getPage("/custom/204") + self.assertStatus(204) + self.assertNoHeader("Content-Length") + self.assertBody("") + self.assertNoHeader("Connection") + + # Make a 304 request on the same connection. + self.getPage("/custom/304") + self.assertStatus(304) + self.assertNoHeader("Content-Length") + self.assertBody("") + self.assertNoHeader("Connection") + + def test_Chunked_Encoding(self): + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + if (hasattr(self, 'harness') and + "modpython" in self.harness.__class__.__name__.lower()): + # mod_python forbids chunked encoding + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + # Set our HTTP_CONN to an instance so it persists between requests. + self.persistent = True + conn = self.HTTP_CONN + + # Try a normal chunked request (with extensions) + body = ntob("8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n" + "Content-Type: application/json\r\n" + "\r\n") + conn.putrequest("POST", "/upload", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Transfer-Encoding", "chunked") + conn.putheader("Trailer", "Content-Type") + # Note that this is somewhat malformed: + # we shouldn't be sending Content-Length. + # RFC 2616 says the server should ignore it. + conn.putheader("Content-Length", "3") + conn.endheaders() + conn.send(body) + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus('200 OK') + self.assertBody("thanks for '%s'" % ntob('xx\r\nxxxxyyyyy')) + + # Try a chunked request that exceeds server.max_request_body_size. + # Note that the delimiters and trailer are included. + body = ntob("3e3\r\n" + ("x" * 995) + "\r\n0\r\n\r\n") + conn.putrequest("POST", "/upload", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Transfer-Encoding", "chunked") + conn.putheader("Content-Type", "text/plain") + # Chunked requests don't need a content-length +## conn.putheader("Content-Length", len(body)) + conn.endheaders() + conn.send(body) + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(413) + conn.close() + + def test_Content_Length_in(self): + # Try a non-chunked request where Content-Length exceeds + # server.max_request_body_size. Assert error before body send. + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest("POST", "/upload", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader("Content-Type", "text/plain") + conn.putheader("Content-Length", "9999") + conn.endheaders() + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(413) + self.assertBody("The entity sent with the request exceeds " + "the maximum allowed bytes.") + conn.close() + + def test_Content_Length_out_preheaders(self): + # Try a non-chunked response where Content-Length is less than + # the actual bytes in the response body. + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest("GET", "/custom_cl?body=I+have+too+many+bytes&cl=5", + skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(500) + self.assertBody( + "The requested resource returned more bytes than the " + "declared Content-Length.") + conn.close() + + def test_Content_Length_out_postheaders(self): + # Try a non-chunked response where Content-Length is less than + # the actual bytes in the response body. + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest("GET", "/custom_cl?body=I+too&body=+have+too+many&cl=5", + skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(200) + self.assertBody("I too") + conn.close() + + def test_598(self): + remote_data_conn = urlopen('%s://%s:%s/one_megabyte_of_a/' % + (self.scheme, self.HOST, self.PORT,)) + buf = remote_data_conn.read(512) + time.sleep(timeout * 0.6) + remaining = (1024 * 1024) - 512 + while remaining: + data = remote_data_conn.read(remaining) + if not data: + break + else: + buf += data + remaining -= len(data) + + self.assertEqual(len(buf), 1024 * 1024) + self.assertEqual(buf, ntob("a" * 1024 * 1024)) + self.assertEqual(remaining, 0) + remote_data_conn.close() + + +class BadRequestTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_No_CRLF(self): + self.persistent = True + + conn = self.HTTP_CONN + conn.send(ntob('GET /hello HTTP/1.1\n\n')) + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.body = response.read() + self.assertBody("HTTP requires CRLF terminators") + conn.close() + + conn.connect() + conn.send(ntob('GET /hello HTTP/1.1\r\n\n')) + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.body = response.read() + self.assertBody("HTTP requires CRLF terminators") + conn.close() + diff --git a/libs/cherrypy/test/test_core.py b/libs/cherrypy/test/test_core.py new file mode 100644 index 0000000..0956a94 --- /dev/null +++ b/libs/cherrypy/test/test_core.py @@ -0,0 +1,688 @@ +"""Basic tests for the CherryPy core: request handling.""" + +import os +localDir = os.path.dirname(__file__) +import sys +import types + +import cherrypy +from cherrypy._cpcompat import IncompleteRead, itervalues, ntob +from cherrypy import _cptools, tools +from cherrypy.lib import httputil, static + + +favicon_path = os.path.join(os.getcwd(), localDir, "../favicon.ico") + +# Client-side code # + +from cherrypy.test import helper + +class CoreRequestHandlingTest(helper.CPWebCase): + + def setup_server(): + class Root: + + def index(self): + return "hello" + index.exposed = True + + favicon_ico = tools.staticfile.handler(filename=favicon_path) + + def defct(self, newct): + newct = "text/%s" % newct + cherrypy.config.update({'tools.response_headers.on': True, + 'tools.response_headers.headers': + [('Content-Type', newct)]}) + defct.exposed = True + + def baseurl(self, path_info, relative=None): + return cherrypy.url(path_info, relative=bool(relative)) + baseurl.exposed = True + + root = Root() + + if sys.version_info >= (2, 5): + from cherrypy.test._test_decorators import ExposeExamples + root.expose_dec = ExposeExamples() + + + class TestType(type): + """Metaclass which automatically exposes all functions in each subclass, + and adds an instance of the subclass as an attribute of root. + """ + def __init__(cls, name, bases, dct): + type.__init__(cls, name, bases, dct) + for value in itervalues(dct): + if isinstance(value, types.FunctionType): + value.exposed = True + setattr(root, name.lower(), cls()) + Test = TestType('Test', (object, ), {}) + + + class URL(Test): + + _cp_config = {'tools.trailing_slash.on': False} + + def index(self, path_info, relative=None): + if relative != 'server': + relative = bool(relative) + return cherrypy.url(path_info, relative=relative) + + def leaf(self, path_info, relative=None): + if relative != 'server': + relative = bool(relative) + return cherrypy.url(path_info, relative=relative) + + + def log_status(): + Status.statuses.append(cherrypy.response.status) + cherrypy.tools.log_status = cherrypy.Tool('on_end_resource', log_status) + + + class Status(Test): + + def index(self): + return "normal" + + def blank(self): + cherrypy.response.status = "" + + # According to RFC 2616, new status codes are OK as long as they + # are between 100 and 599. + + # Here is an illegal code... + def illegal(self): + cherrypy.response.status = 781 + return "oops" + + # ...and here is an unknown but legal code. + def unknown(self): + cherrypy.response.status = "431 My custom error" + return "funky" + + # Non-numeric code + def bad(self): + cherrypy.response.status = "error" + return "bad news" + + statuses = [] + def on_end_resource_stage(self): + return repr(self.statuses) + on_end_resource_stage._cp_config = {'tools.log_status.on': True} + + + class Redirect(Test): + + class Error: + _cp_config = {"tools.err_redirect.on": True, + "tools.err_redirect.url": "/errpage", + "tools.err_redirect.internal": False, + } + + def index(self): + raise NameError("redirect_test") + index.exposed = True + error = Error() + + def index(self): + return "child" + + def custom(self, url, code): + raise cherrypy.HTTPRedirect(url, code) + + def by_code(self, code): + raise cherrypy.HTTPRedirect("somewhere%20else", code) + by_code._cp_config = {'tools.trailing_slash.extra': True} + + def nomodify(self): + raise cherrypy.HTTPRedirect("", 304) + + def proxy(self): + raise cherrypy.HTTPRedirect("proxy", 305) + + def stringify(self): + return str(cherrypy.HTTPRedirect("/")) + + def fragment(self, frag): + raise cherrypy.HTTPRedirect("/some/url#%s" % frag) + + def login_redir(): + if not getattr(cherrypy.request, "login", None): + raise cherrypy.InternalRedirect("/internalredirect/login") + tools.login_redir = _cptools.Tool('before_handler', login_redir) + + def redir_custom(): + raise cherrypy.InternalRedirect("/internalredirect/custom_err") + + class InternalRedirect(Test): + + def index(self): + raise cherrypy.InternalRedirect("/") + + def choke(self): + return 3 / 0 + choke.exposed = True + choke._cp_config = {'hooks.before_error_response': redir_custom} + + def relative(self, a, b): + raise cherrypy.InternalRedirect("cousin?t=6") + + def cousin(self, t): + assert cherrypy.request.prev.closed + return cherrypy.request.prev.query_string + + def petshop(self, user_id): + if user_id == "parrot": + # Trade it for a slug when redirecting + raise cherrypy.InternalRedirect('/image/getImagesByUser?user_id=slug') + elif user_id == "terrier": + # Trade it for a fish when redirecting + raise cherrypy.InternalRedirect('/image/getImagesByUser?user_id=fish') + else: + # This should pass the user_id through to getImagesByUser + raise cherrypy.InternalRedirect( + '/image/getImagesByUser?user_id=%s' % str(user_id)) + + # We support Python 2.3, but the @-deco syntax would look like this: + # @tools.login_redir() + def secure(self): + return "Welcome!" + secure = tools.login_redir()(secure) + # Since calling the tool returns the same function you pass in, + # you could skip binding the return value, and just write: + # tools.login_redir()(secure) + + def login(self): + return "Please log in" + + def custom_err(self): + return "Something went horribly wrong." + + def early_ir(self, arg): + return "whatever" + early_ir._cp_config = {'hooks.before_request_body': redir_custom} + + + class Image(Test): + + def getImagesByUser(self, user_id): + return "0 images for %s" % user_id + + + class Flatten(Test): + + def as_string(self): + return "content" + + def as_list(self): + return ["con", "tent"] + + def as_yield(self): + yield ntob("content") + + def as_dblyield(self): + yield self.as_yield() + as_dblyield._cp_config = {'tools.flatten.on': True} + + def as_refyield(self): + for chunk in self.as_yield(): + yield chunk + + + class Ranges(Test): + + def get_ranges(self, bytes): + return repr(httputil.get_ranges('bytes=%s' % bytes, 8)) + + def slice_file(self): + path = os.path.join(os.getcwd(), os.path.dirname(__file__)) + return static.serve_file(os.path.join(path, "static/index.html")) + + + class Cookies(Test): + + def single(self, name): + cookie = cherrypy.request.cookie[name] + # Python2's SimpleCookie.__setitem__ won't take unicode keys. + cherrypy.response.cookie[str(name)] = cookie.value + + def multiple(self, names): + for name in names: + cookie = cherrypy.request.cookie[name] + # Python2's SimpleCookie.__setitem__ won't take unicode keys. + cherrypy.response.cookie[str(name)] = cookie.value + + def append_headers(header_list, debug=False): + if debug: + cherrypy.log( + "Extending response headers with %s" % repr(header_list), + "TOOLS.APPEND_HEADERS") + cherrypy.serving.response.header_list.extend(header_list) + cherrypy.tools.append_headers = cherrypy.Tool('on_end_resource', append_headers) + + class MultiHeader(Test): + + def header_list(self): + pass + header_list = cherrypy.tools.append_headers(header_list=[ + (ntob('WWW-Authenticate'), ntob('Negotiate')), + (ntob('WWW-Authenticate'), ntob('Basic realm="foo"')), + ])(header_list) + + def commas(self): + cherrypy.response.headers['WWW-Authenticate'] = 'Negotiate,Basic realm="foo"' + + + cherrypy.tree.mount(root) + setup_server = staticmethod(setup_server) + + + def testStatus(self): + self.getPage("/status/") + self.assertBody('normal') + self.assertStatus(200) + + self.getPage("/status/blank") + self.assertBody('') + self.assertStatus(200) + + self.getPage("/status/illegal") + self.assertStatus(500) + msg = "Illegal response status from server (781 is out of range)." + self.assertErrorPage(500, msg) + + if not getattr(cherrypy.server, 'using_apache', False): + self.getPage("/status/unknown") + self.assertBody('funky') + self.assertStatus(431) + + self.getPage("/status/bad") + self.assertStatus(500) + msg = "Illegal response status from server ('error' is non-numeric)." + self.assertErrorPage(500, msg) + + def test_on_end_resource_status(self): + self.getPage('/status/on_end_resource_stage') + self.assertBody('[]') + self.getPage('/status/on_end_resource_stage') + self.assertBody(repr(["200 OK"])) + + def testSlashes(self): + # Test that requests for index methods without a trailing slash + # get redirected to the same URI path with a trailing slash. + # Make sure GET params are preserved. + self.getPage("/redirect?id=3") + self.assertStatus(301) + self.assertInBody("" + "%s/redirect/?id=3" % (self.base(), self.base())) + + if self.prefix(): + # Corner case: the "trailing slash" redirect could be tricky if + # we're using a virtual root and the URI is "/vroot" (no slash). + self.getPage("") + self.assertStatus(301) + self.assertInBody("%s/" % + (self.base(), self.base())) + + # Test that requests for NON-index methods WITH a trailing slash + # get redirected to the same URI path WITHOUT a trailing slash. + # Make sure GET params are preserved. + self.getPage("/redirect/by_code/?code=307") + self.assertStatus(301) + self.assertInBody("" + "%s/redirect/by_code?code=307" + % (self.base(), self.base())) + + # If the trailing_slash tool is off, CP should just continue + # as if the slashes were correct. But it needs some help + # inside cherrypy.url to form correct output. + self.getPage('/url?path_info=page1') + self.assertBody('%s/url/page1' % self.base()) + self.getPage('/url/leaf/?path_info=page1') + self.assertBody('%s/url/page1' % self.base()) + + def testRedirect(self): + self.getPage("/redirect/") + self.assertBody('child') + self.assertStatus(200) + + self.getPage("/redirect/by_code?code=300") + self.assertMatchesBody(r"\1somewhere%20else") + self.assertStatus(300) + + self.getPage("/redirect/by_code?code=301") + self.assertMatchesBody(r"\1somewhere%20else") + self.assertStatus(301) + + self.getPage("/redirect/by_code?code=302") + self.assertMatchesBody(r"\1somewhere%20else") + self.assertStatus(302) + + self.getPage("/redirect/by_code?code=303") + self.assertMatchesBody(r"\1somewhere%20else") + self.assertStatus(303) + + self.getPage("/redirect/by_code?code=307") + self.assertMatchesBody(r"\1somewhere%20else") + self.assertStatus(307) + + self.getPage("/redirect/nomodify") + self.assertBody('') + self.assertStatus(304) + + self.getPage("/redirect/proxy") + self.assertBody('') + self.assertStatus(305) + + # HTTPRedirect on error + self.getPage("/redirect/error/") + self.assertStatus(('302 Found', '303 See Other')) + self.assertInBody('/errpage') + + # Make sure str(HTTPRedirect()) works. + self.getPage("/redirect/stringify", protocol="HTTP/1.0") + self.assertStatus(200) + self.assertBody("(['%s/'], 302)" % self.base()) + if cherrypy.server.protocol_version == "HTTP/1.1": + self.getPage("/redirect/stringify", protocol="HTTP/1.1") + self.assertStatus(200) + self.assertBody("(['%s/'], 303)" % self.base()) + + # check that #fragments are handled properly + # http://skrb.org/ietf/http_errata.html#location-fragments + frag = "foo" + self.getPage("/redirect/fragment/%s" % frag) + self.assertMatchesBody(r"\1\/some\/url\#%s" % (frag, frag)) + loc = self.assertHeader('Location') + assert loc.endswith("#%s" % frag) + self.assertStatus(('302 Found', '303 See Other')) + + # check injection protection + # See http://www.cherrypy.org/ticket/1003 + self.getPage("/redirect/custom?code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval") + self.assertStatus(303) + loc = self.assertHeader('Location') + assert 'Set-Cookie' in loc + self.assertNoHeader('Set-Cookie') + + def test_InternalRedirect(self): + # InternalRedirect + self.getPage("/internalredirect/") + self.assertBody('hello') + self.assertStatus(200) + + # Test passthrough + self.getPage("/internalredirect/petshop?user_id=Sir-not-appearing-in-this-film") + self.assertBody('0 images for Sir-not-appearing-in-this-film') + self.assertStatus(200) + + # Test args + self.getPage("/internalredirect/petshop?user_id=parrot") + self.assertBody('0 images for slug') + self.assertStatus(200) + + # Test POST + self.getPage("/internalredirect/petshop", method="POST", + body="user_id=terrier") + self.assertBody('0 images for fish') + self.assertStatus(200) + + # Test ir before body read + self.getPage("/internalredirect/early_ir", method="POST", + body="arg=aha!") + self.assertBody("Something went horribly wrong.") + self.assertStatus(200) + + self.getPage("/internalredirect/secure") + self.assertBody('Please log in') + self.assertStatus(200) + + # Relative path in InternalRedirect. + # Also tests request.prev. + self.getPage("/internalredirect/relative?a=3&b=5") + self.assertBody("a=3&b=5") + self.assertStatus(200) + + # InternalRedirect on error + self.getPage("/internalredirect/choke") + self.assertStatus(200) + self.assertBody("Something went horribly wrong.") + + def testFlatten(self): + for url in ["/flatten/as_string", "/flatten/as_list", + "/flatten/as_yield", "/flatten/as_dblyield", + "/flatten/as_refyield"]: + self.getPage(url) + self.assertBody('content') + + def testRanges(self): + self.getPage("/ranges/get_ranges?bytes=3-6") + self.assertBody("[(3, 7)]") + + # Test multiple ranges and a suffix-byte-range-spec, for good measure. + self.getPage("/ranges/get_ranges?bytes=2-4,-1") + self.assertBody("[(2, 5), (7, 8)]") + + # Get a partial file. + if cherrypy.server.protocol_version == "HTTP/1.1": + self.getPage("/ranges/slice_file", [('Range', 'bytes=2-5')]) + self.assertStatus(206) + self.assertHeader("Content-Type", "text/html;charset=utf-8") + self.assertHeader("Content-Range", "bytes 2-5/14") + self.assertBody("llo,") + + # What happens with overlapping ranges (and out of order, too)? + self.getPage("/ranges/slice_file", [('Range', 'bytes=4-6,2-5')]) + self.assertStatus(206) + ct = self.assertHeader("Content-Type") + expected_type = "multipart/byteranges; boundary=" + self.assert_(ct.startswith(expected_type)) + boundary = ct[len(expected_type):] + expected_body = ("\r\n--%s\r\n" + "Content-type: text/html\r\n" + "Content-range: bytes 4-6/14\r\n" + "\r\n" + "o, \r\n" + "--%s\r\n" + "Content-type: text/html\r\n" + "Content-range: bytes 2-5/14\r\n" + "\r\n" + "llo,\r\n" + "--%s--\r\n" % (boundary, boundary, boundary)) + self.assertBody(expected_body) + self.assertHeader("Content-Length") + + # Test "416 Requested Range Not Satisfiable" + self.getPage("/ranges/slice_file", [('Range', 'bytes=2300-2900')]) + self.assertStatus(416) + # "When this status code is returned for a byte-range request, + # the response SHOULD include a Content-Range entity-header + # field specifying the current length of the selected resource" + self.assertHeader("Content-Range", "bytes */14") + elif cherrypy.server.protocol_version == "HTTP/1.0": + # Test Range behavior with HTTP/1.0 request + self.getPage("/ranges/slice_file", [('Range', 'bytes=2-5')]) + self.assertStatus(200) + self.assertBody("Hello, world\r\n") + + def testFavicon(self): + # favicon.ico is served by staticfile. + icofilename = os.path.join(localDir, "../favicon.ico") + icofile = open(icofilename, "rb") + data = icofile.read() + icofile.close() + + self.getPage("/favicon.ico") + self.assertBody(data) + + def testCookies(self): + if sys.version_info >= (2, 5): + header_value = lambda x: x + else: + header_value = lambda x: x+';' + + self.getPage("/cookies/single?name=First", + [('Cookie', 'First=Dinsdale;')]) + self.assertHeader('Set-Cookie', header_value('First=Dinsdale')) + + self.getPage("/cookies/multiple?names=First&names=Last", + [('Cookie', 'First=Dinsdale; Last=Piranha;'), + ]) + self.assertHeader('Set-Cookie', header_value('First=Dinsdale')) + self.assertHeader('Set-Cookie', header_value('Last=Piranha')) + + self.getPage("/cookies/single?name=Something-With:Colon", + [('Cookie', 'Something-With:Colon=some-value')]) + self.assertStatus(400) + + def testDefaultContentType(self): + self.getPage('/') + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.getPage('/defct/plain') + self.getPage('/') + self.assertHeader('Content-Type', 'text/plain;charset=utf-8') + self.getPage('/defct/html') + + def test_multiple_headers(self): + self.getPage('/multiheader/header_list') + self.assertEqual([(k, v) for k, v in self.headers if k == 'WWW-Authenticate'], + [('WWW-Authenticate', 'Negotiate'), + ('WWW-Authenticate', 'Basic realm="foo"'), + ]) + self.getPage('/multiheader/commas') + self.assertHeader('WWW-Authenticate', 'Negotiate,Basic realm="foo"') + + def test_cherrypy_url(self): + # Input relative to current + self.getPage('/url/leaf?path_info=page1') + self.assertBody('%s/url/page1' % self.base()) + self.getPage('/url/?path_info=page1') + self.assertBody('%s/url/page1' % self.base()) + # Other host header + host = 'www.mydomain.example' + self.getPage('/url/leaf?path_info=page1', + headers=[('Host', host)]) + self.assertBody('%s://%s/url/page1' % (self.scheme, host)) + + # Input is 'absolute'; that is, relative to script_name + self.getPage('/url/leaf?path_info=/page1') + self.assertBody('%s/page1' % self.base()) + self.getPage('/url/?path_info=/page1') + self.assertBody('%s/page1' % self.base()) + + # Single dots + self.getPage('/url/leaf?path_info=./page1') + self.assertBody('%s/url/page1' % self.base()) + self.getPage('/url/leaf?path_info=other/./page1') + self.assertBody('%s/url/other/page1' % self.base()) + self.getPage('/url/?path_info=/other/./page1') + self.assertBody('%s/other/page1' % self.base()) + + # Double dots + self.getPage('/url/leaf?path_info=../page1') + self.assertBody('%s/page1' % self.base()) + self.getPage('/url/leaf?path_info=other/../page1') + self.assertBody('%s/url/page1' % self.base()) + self.getPage('/url/leaf?path_info=/other/../page1') + self.assertBody('%s/page1' % self.base()) + + # Output relative to current path or script_name + self.getPage('/url/?path_info=page1&relative=True') + self.assertBody('page1') + self.getPage('/url/leaf?path_info=/page1&relative=True') + self.assertBody('../page1') + self.getPage('/url/leaf?path_info=page1&relative=True') + self.assertBody('page1') + self.getPage('/url/leaf?path_info=leaf/page1&relative=True') + self.assertBody('leaf/page1') + self.getPage('/url/leaf?path_info=../page1&relative=True') + self.assertBody('../page1') + self.getPage('/url/?path_info=other/../page1&relative=True') + self.assertBody('page1') + + # Output relative to / + self.getPage('/baseurl?path_info=ab&relative=True') + self.assertBody('ab') + # Output relative to / + self.getPage('/baseurl?path_info=/ab&relative=True') + self.assertBody('ab') + + # absolute-path references ("server-relative") + # Input relative to current + self.getPage('/url/leaf?path_info=page1&relative=server') + self.assertBody('/url/page1') + self.getPage('/url/?path_info=page1&relative=server') + self.assertBody('/url/page1') + # Input is 'absolute'; that is, relative to script_name + self.getPage('/url/leaf?path_info=/page1&relative=server') + self.assertBody('/page1') + self.getPage('/url/?path_info=/page1&relative=server') + self.assertBody('/page1') + + def test_expose_decorator(self): + if not sys.version_info >= (2, 5): + return self.skip("skipped (Python 2.5+ only) ") + + # Test @expose + self.getPage("/expose_dec/no_call") + self.assertStatus(200) + self.assertBody("Mr E. R. Bradshaw") + + # Test @expose() + self.getPage("/expose_dec/call_empty") + self.assertStatus(200) + self.assertBody("Mrs. B.J. Smegma") + + # Test @expose("alias") + self.getPage("/expose_dec/call_alias") + self.assertStatus(200) + self.assertBody("Mr Nesbitt") + # Does the original name work? + self.getPage("/expose_dec/nesbitt") + self.assertStatus(200) + self.assertBody("Mr Nesbitt") + + # Test @expose(["alias1", "alias2"]) + self.getPage("/expose_dec/alias1") + self.assertStatus(200) + self.assertBody("Mr Ken Andrews") + self.getPage("/expose_dec/alias2") + self.assertStatus(200) + self.assertBody("Mr Ken Andrews") + # Does the original name work? + self.getPage("/expose_dec/andrews") + self.assertStatus(200) + self.assertBody("Mr Ken Andrews") + + # Test @expose(alias="alias") + self.getPage("/expose_dec/alias3") + self.assertStatus(200) + self.assertBody("Mr. and Mrs. Watson") + + +class ErrorTests(helper.CPWebCase): + + def setup_server(): + def break_header(): + # Add a header after finalize that is invalid + cherrypy.serving.response.header_list.append((2, 3)) + cherrypy.tools.break_header = cherrypy.Tool('on_end_resource', break_header) + + class Root: + def index(self): + return "hello" + index.exposed = True + + def start_response_error(self): + return "salud!" + start_response_error._cp_config = {'tools.break_header.on': True} + root = Root() + + cherrypy.tree.mount(root) + setup_server = staticmethod(setup_server) + + def test_start_response_error(self): + self.getPage("/start_response_error") + self.assertStatus(500) + self.assertInBody("TypeError: response.header_list key 2 is not a byte string.") + diff --git a/libs/cherrypy/test/test_dynamicobjectmapping.py b/libs/cherrypy/test/test_dynamicobjectmapping.py new file mode 100644 index 0000000..0395b7b --- /dev/null +++ b/libs/cherrypy/test/test_dynamicobjectmapping.py @@ -0,0 +1,404 @@ +import cherrypy +from cherrypy._cpcompat import sorted, unicodestr +from cherrypy._cptree import Application +from cherrypy.test import helper + +script_names = ["", "/foo", "/users/fred/blog", "/corp/blog"] + + + +def setup_server(): + class SubSubRoot: + def index(self): + return "SubSubRoot index" + index.exposed = True + + def default(self, *args): + return "SubSubRoot default" + default.exposed = True + + def handler(self): + return "SubSubRoot handler" + handler.exposed = True + + def dispatch(self): + return "SubSubRoot dispatch" + dispatch.exposed = True + + subsubnodes = { + '1': SubSubRoot(), + '2': SubSubRoot(), + } + + class SubRoot: + def index(self): + return "SubRoot index" + index.exposed = True + + def default(self, *args): + return "SubRoot %s" % (args,) + default.exposed = True + + def handler(self): + return "SubRoot handler" + handler.exposed = True + + def _cp_dispatch(self, vpath): + return subsubnodes.get(vpath[0], None) + + subnodes = { + '1': SubRoot(), + '2': SubRoot(), + } + class Root: + def index(self): + return "index" + index.exposed = True + + def default(self, *args): + return "default %s" % (args,) + default.exposed = True + + def handler(self): + return "handler" + handler.exposed = True + + def _cp_dispatch(self, vpath): + return subnodes.get(vpath[0]) + + #-------------------------------------------------------------------------- + # DynamicNodeAndMethodDispatcher example. + # This example exposes a fairly naive HTTP api + class User(object): + def __init__(self, id, name): + self.id = id + self.name = name + + def __unicode__(self): + return unicode(self.name) + def __str__(self): + return str(self.name) + + user_lookup = { + 1: User(1, 'foo'), + 2: User(2, 'bar'), + } + + def make_user(name, id=None): + if not id: + id = max(*list(user_lookup.keys())) + 1 + user_lookup[id] = User(id, name) + return id + + class UserContainerNode(object): + exposed = True + + def POST(self, name): + """ + Allow the creation of a new Object + """ + return "POST %d" % make_user(name) + + def GET(self): + return unicodestr(sorted(user_lookup.keys())) + + def dynamic_dispatch(self, vpath): + try: + id = int(vpath[0]) + except (ValueError, IndexError): + return None + return UserInstanceNode(id) + + class UserInstanceNode(object): + exposed = True + def __init__(self, id): + self.id = id + self.user = user_lookup.get(id, None) + + # For all but PUT methods there MUST be a valid user identified + # by self.id + if not self.user and cherrypy.request.method != 'PUT': + raise cherrypy.HTTPError(404) + + def GET(self, *args, **kwargs): + """ + Return the appropriate representation of the instance. + """ + return unicodestr(self.user) + + def POST(self, name): + """ + Update the fields of the user instance. + """ + self.user.name = name + return "POST %d" % self.user.id + + def PUT(self, name): + """ + Create a new user with the specified id, or edit it if it already exists + """ + if self.user: + # Edit the current user + self.user.name = name + return "PUT %d" % self.user.id + else: + # Make a new user with said attributes. + return "PUT %d" % make_user(name, self.id) + + def DELETE(self): + """ + Delete the user specified at the id. + """ + id = self.user.id + del user_lookup[self.user.id] + del self.user + return "DELETE %d" % id + + + class ABHandler: + class CustomDispatch: + def index(self, a, b): + return "custom" + index.exposed = True + + def _cp_dispatch(self, vpath): + """Make sure that if we don't pop anything from vpath, + processing still works. + """ + return self.CustomDispatch() + + def index(self, a, b=None): + body = [ 'a:' + str(a) ] + if b is not None: + body.append(',b:' + str(b)) + return ''.join(body) + index.exposed = True + + def delete(self, a, b): + return 'deleting ' + str(a) + ' and ' + str(b) + delete.exposed = True + + class IndexOnly: + def _cp_dispatch(self, vpath): + """Make sure that popping ALL of vpath still shows the index + handler. + """ + while vpath: + vpath.pop() + return self + + def index(self): + return "IndexOnly index" + index.exposed = True + + class DecoratedPopArgs: + """Test _cp_dispatch with @cherrypy.popargs.""" + def index(self): + return "no params" + index.exposed = True + + def hi(self): + return "hi was not interpreted as 'a' param" + hi.exposed = True + DecoratedPopArgs = cherrypy.popargs('a', 'b', handler=ABHandler())(DecoratedPopArgs) + + class NonDecoratedPopArgs: + """Test _cp_dispatch = cherrypy.popargs()""" + + _cp_dispatch = cherrypy.popargs('a') + + def index(self, a): + return "index: " + str(a) + index.exposed = True + + class ParameterizedHandler: + """Special handler created for each request""" + + def __init__(self, a): + self.a = a + + def index(self): + if 'a' in cherrypy.request.params: + raise Exception("Parameterized handler argument ended up in request.params") + return self.a + index.exposed = True + + class ParameterizedPopArgs: + """Test cherrypy.popargs() with a function call handler""" + ParameterizedPopArgs = cherrypy.popargs('a', handler=ParameterizedHandler)(ParameterizedPopArgs) + + Root.decorated = DecoratedPopArgs() + Root.undecorated = NonDecoratedPopArgs() + Root.index_only = IndexOnly() + Root.parameter_test = ParameterizedPopArgs() + + Root.users = UserContainerNode() + + md = cherrypy.dispatch.MethodDispatcher('dynamic_dispatch') + for url in script_names: + conf = {'/': { + 'user': (url or "/").split("/")[-2], + }, + '/users': { + 'request.dispatch': md + }, + } + cherrypy.tree.mount(Root(), url, conf) + +class DynamicObjectMappingTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def testObjectMapping(self): + for url in script_names: + prefix = self.script_name = url + + self.getPage('/') + self.assertBody('index') + + self.getPage('/handler') + self.assertBody('handler') + + # Dynamic dispatch will succeed here for the subnodes + # so the subroot gets called + self.getPage('/1/') + self.assertBody('SubRoot index') + + self.getPage('/2/') + self.assertBody('SubRoot index') + + self.getPage('/1/handler') + self.assertBody('SubRoot handler') + + self.getPage('/2/handler') + self.assertBody('SubRoot handler') + + # Dynamic dispatch will fail here for the subnodes + # so the default gets called + self.getPage('/asdf/') + self.assertBody("default ('asdf',)") + + self.getPage('/asdf/asdf') + self.assertBody("default ('asdf', 'asdf')") + + self.getPage('/asdf/handler') + self.assertBody("default ('asdf', 'handler')") + + # Dynamic dispatch will succeed here for the subsubnodes + # so the subsubroot gets called + self.getPage('/1/1/') + self.assertBody('SubSubRoot index') + + self.getPage('/2/2/') + self.assertBody('SubSubRoot index') + + self.getPage('/1/1/handler') + self.assertBody('SubSubRoot handler') + + self.getPage('/2/2/handler') + self.assertBody('SubSubRoot handler') + + self.getPage('/2/2/dispatch') + self.assertBody('SubSubRoot dispatch') + + # The exposed dispatch will not be called as a dispatch + # method. + self.getPage('/2/2/foo/foo') + self.assertBody("SubSubRoot default") + + # Dynamic dispatch will fail here for the subsubnodes + # so the SubRoot gets called + self.getPage('/1/asdf/') + self.assertBody("SubRoot ('asdf',)") + + self.getPage('/1/asdf/asdf') + self.assertBody("SubRoot ('asdf', 'asdf')") + + self.getPage('/1/asdf/handler') + self.assertBody("SubRoot ('asdf', 'handler')") + + def testMethodDispatch(self): + # GET acts like a container + self.getPage("/users") + self.assertBody("[1, 2]") + self.assertHeader('Allow', 'GET, HEAD, POST') + + # POST to the container URI allows creation + self.getPage("/users", method="POST", body="name=baz") + self.assertBody("POST 3") + self.assertHeader('Allow', 'GET, HEAD, POST') + + # POST to a specific instanct URI results in a 404 + # as the resource does not exit. + self.getPage("/users/5", method="POST", body="name=baz") + self.assertStatus(404) + + # PUT to a specific instanct URI results in creation + self.getPage("/users/5", method="PUT", body="name=boris") + self.assertBody("PUT 5") + self.assertHeader('Allow', 'DELETE, GET, HEAD, POST, PUT') + + # GET acts like a container + self.getPage("/users") + self.assertBody("[1, 2, 3, 5]") + self.assertHeader('Allow', 'GET, HEAD, POST') + + test_cases = ( + (1, 'foo', 'fooupdated', 'DELETE, GET, HEAD, POST, PUT'), + (2, 'bar', 'barupdated', 'DELETE, GET, HEAD, POST, PUT'), + (3, 'baz', 'bazupdated', 'DELETE, GET, HEAD, POST, PUT'), + (5, 'boris', 'borisupdated', 'DELETE, GET, HEAD, POST, PUT'), + ) + for id, name, updatedname, headers in test_cases: + self.getPage("/users/%d" % id) + self.assertBody(name) + self.assertHeader('Allow', headers) + + # Make sure POSTs update already existings resources + self.getPage("/users/%d" % id, method='POST', body="name=%s" % updatedname) + self.assertBody("POST %d" % id) + self.assertHeader('Allow', headers) + + # Make sure PUTs Update already existing resources. + self.getPage("/users/%d" % id, method='PUT', body="name=%s" % updatedname) + self.assertBody("PUT %d" % id) + self.assertHeader('Allow', headers) + + # Make sure DELETES Remove already existing resources. + self.getPage("/users/%d" % id, method='DELETE') + self.assertBody("DELETE %d" % id) + self.assertHeader('Allow', headers) + + + # GET acts like a container + self.getPage("/users") + self.assertBody("[]") + self.assertHeader('Allow', 'GET, HEAD, POST') + + def testVpathDispatch(self): + self.getPage("/decorated/") + self.assertBody("no params") + + self.getPage("/decorated/hi") + self.assertBody("hi was not interpreted as 'a' param") + + self.getPage("/decorated/yo/") + self.assertBody("a:yo") + + self.getPage("/decorated/yo/there/") + self.assertBody("a:yo,b:there") + + self.getPage("/decorated/yo/there/delete") + self.assertBody("deleting yo and there") + + self.getPage("/decorated/yo/there/handled_by_dispatch/") + self.assertBody("custom") + + self.getPage("/undecorated/blah/") + self.assertBody("index: blah") + + self.getPage("/index_only/a/b/c/d/e/f/g/") + self.assertBody("IndexOnly index") + + self.getPage("/parameter_test/argument2/") + self.assertBody("argument2") + diff --git a/libs/cherrypy/test/test_encoding.py b/libs/cherrypy/test/test_encoding.py new file mode 100644 index 0000000..2d0ce76 --- /dev/null +++ b/libs/cherrypy/test/test_encoding.py @@ -0,0 +1,363 @@ + +import gzip +import sys + +import cherrypy +from cherrypy._cpcompat import BytesIO, IncompleteRead, ntob, ntou + +europoundUnicode = ntou('\x80\xa3') +sing = ntou("\u6bdb\u6cfd\u4e1c: Sing, Little Birdie?", 'escape') +sing8 = sing.encode('utf-8') +sing16 = sing.encode('utf-16') + + +from cherrypy.test import helper + + +class EncodingTests(helper.CPWebCase): + + def setup_server(): + class Root: + def index(self, param): + assert param == europoundUnicode, "%r != %r" % (param, europoundUnicode) + yield europoundUnicode + index.exposed = True + + def mao_zedong(self): + return sing + mao_zedong.exposed = True + + def utf8(self): + return sing8 + utf8.exposed = True + utf8._cp_config = {'tools.encode.encoding': 'utf-8'} + + def cookies_and_headers(self): + # if the headers have non-ascii characters and a cookie has + # any part which is unicode (even ascii), the response + # should not fail. + cherrypy.response.cookie['candy'] = 'bar' + cherrypy.response.cookie['candy']['domain'] = 'cherrypy.org' + cherrypy.response.headers['Some-Header'] = 'My d\xc3\xb6g has fleas' + return 'Any content' + cookies_and_headers.exposed = True + + def reqparams(self, *args, **kwargs): + return ntob(', ').join([": ".join((k, v)).encode('utf8') + for k, v in cherrypy.request.params.items()]) + reqparams.exposed = True + + def nontext(self, *args, **kwargs): + cherrypy.response.headers['Content-Type'] = 'application/binary' + return '\x00\x01\x02\x03' + nontext.exposed = True + nontext._cp_config = {'tools.encode.text_only': False, + 'tools.encode.add_charset': True, + } + + class GZIP: + def index(self): + yield "Hello, world" + index.exposed = True + + def noshow(self): + # Test for ticket #147, where yield showed no exceptions (content- + # encoding was still gzip even though traceback wasn't zipped). + raise IndexError() + yield "Here be dragons" + noshow.exposed = True + # Turn encoding off so the gzip tool is the one doing the collapse. + noshow._cp_config = {'tools.encode.on': False} + + def noshow_stream(self): + # Test for ticket #147, where yield showed no exceptions (content- + # encoding was still gzip even though traceback wasn't zipped). + raise IndexError() + yield "Here be dragons" + noshow_stream.exposed = True + noshow_stream._cp_config = {'response.stream': True} + + class Decode: + def extra_charset(self, *args, **kwargs): + return ', '.join([": ".join((k, v)) + for k, v in cherrypy.request.params.items()]) + extra_charset.exposed = True + extra_charset._cp_config = { + 'tools.decode.on': True, + 'tools.decode.default_encoding': ['utf-16'], + } + + def force_charset(self, *args, **kwargs): + return ', '.join([": ".join((k, v)) + for k, v in cherrypy.request.params.items()]) + force_charset.exposed = True + force_charset._cp_config = { + 'tools.decode.on': True, + 'tools.decode.encoding': 'utf-16', + } + + root = Root() + root.gzip = GZIP() + root.decode = Decode() + cherrypy.tree.mount(root, config={'/gzip': {'tools.gzip.on': True}}) + setup_server = staticmethod(setup_server) + + def test_query_string_decoding(self): + europoundUtf8 = europoundUnicode.encode('utf-8') + self.getPage(ntob('/?param=') + europoundUtf8) + self.assertBody(europoundUtf8) + + # Encoded utf8 query strings MUST be parsed correctly. + # Here, q is the POUND SIGN U+00A3 encoded in utf8 and then %HEX + self.getPage("/reqparams?q=%C2%A3") + # The return value will be encoded as utf8. + self.assertBody(ntob("q: \xc2\xa3")) + + # Query strings that are incorrectly encoded MUST raise 404. + # Here, q is the POUND SIGN U+00A3 encoded in latin1 and then %HEX + self.getPage("/reqparams?q=%A3") + self.assertStatus(404) + self.assertErrorPage(404, + "The given query string could not be processed. Query " + "strings for this resource must be encoded with 'utf8'.") + + def test_urlencoded_decoding(self): + # Test the decoding of an application/x-www-form-urlencoded entity. + europoundUtf8 = europoundUnicode.encode('utf-8') + body=ntob("param=") + europoundUtf8 + self.getPage('/', method='POST', + headers=[("Content-Type", "application/x-www-form-urlencoded"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertBody(europoundUtf8) + + # Encoded utf8 entities MUST be parsed and decoded correctly. + # Here, q is the POUND SIGN U+00A3 encoded in utf8 + body = ntob("q=\xc2\xa3") + self.getPage('/reqparams', method='POST', + headers=[("Content-Type", "application/x-www-form-urlencoded"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertBody(ntob("q: \xc2\xa3")) + + # ...and in utf16, which is not in the default attempt_charsets list: + body = ntob("\xff\xfeq\x00=\xff\xfe\xa3\x00") + self.getPage('/reqparams', method='POST', + headers=[("Content-Type", "application/x-www-form-urlencoded;charset=utf-16"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertBody(ntob("q: \xc2\xa3")) + + # Entities that are incorrectly encoded MUST raise 400. + # Here, q is the POUND SIGN U+00A3 encoded in utf16, but + # the Content-Type incorrectly labels it utf-8. + body = ntob("\xff\xfeq\x00=\xff\xfe\xa3\x00") + self.getPage('/reqparams', method='POST', + headers=[("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertStatus(400) + self.assertErrorPage(400, + "The request entity could not be decoded. The following charsets " + "were attempted: ['utf-8']") + + def test_decode_tool(self): + # An extra charset should be tried first, and succeed if it matches. + # Here, we add utf-16 as a charset and pass a utf-16 body. + body = ntob("\xff\xfeq\x00=\xff\xfe\xa3\x00") + self.getPage('/decode/extra_charset', method='POST', + headers=[("Content-Type", "application/x-www-form-urlencoded"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertBody(ntob("q: \xc2\xa3")) + + # An extra charset should be tried first, and continue to other default + # charsets if it doesn't match. + # Here, we add utf-16 as a charset but still pass a utf-8 body. + body = ntob("q=\xc2\xa3") + self.getPage('/decode/extra_charset', method='POST', + headers=[("Content-Type", "application/x-www-form-urlencoded"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertBody(ntob("q: \xc2\xa3")) + + # An extra charset should error if force is True and it doesn't match. + # Here, we force utf-16 as a charset but still pass a utf-8 body. + body = ntob("q=\xc2\xa3") + self.getPage('/decode/force_charset', method='POST', + headers=[("Content-Type", "application/x-www-form-urlencoded"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertErrorPage(400, + "The request entity could not be decoded. The following charsets " + "were attempted: ['utf-16']") + + def test_multipart_decoding(self): + # Test the decoding of a multipart entity when the charset (utf16) is + # explicitly given. + body=ntob('\r\n'.join(['--X', + 'Content-Type: text/plain;charset=utf-16', + 'Content-Disposition: form-data; name="text"', + '', + '\xff\xfea\x00b\x00\x1c c\x00', + '--X', + 'Content-Type: text/plain;charset=utf-16', + 'Content-Disposition: form-data; name="submit"', + '', + '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00', + '--X--'])) + self.getPage('/reqparams', method='POST', + headers=[("Content-Type", "multipart/form-data;boundary=X"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertBody(ntob("text: ab\xe2\x80\x9cc, submit: Create")) + + def test_multipart_decoding_no_charset(self): + # Test the decoding of a multipart entity when the charset (utf8) is + # NOT explicitly given, but is in the list of charsets to attempt. + body=ntob('\r\n'.join(['--X', + 'Content-Disposition: form-data; name="text"', + '', + '\xe2\x80\x9c', + '--X', + 'Content-Disposition: form-data; name="submit"', + '', + 'Create', + '--X--'])) + self.getPage('/reqparams', method='POST', + headers=[("Content-Type", "multipart/form-data;boundary=X"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertBody(ntob("text: \xe2\x80\x9c, submit: Create")) + + def test_multipart_decoding_no_successful_charset(self): + # Test the decoding of a multipart entity when the charset (utf16) is + # NOT explicitly given, and is NOT in the list of charsets to attempt. + body=ntob('\r\n'.join(['--X', + 'Content-Disposition: form-data; name="text"', + '', + '\xff\xfea\x00b\x00\x1c c\x00', + '--X', + 'Content-Disposition: form-data; name="submit"', + '', + '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00', + '--X--'])) + self.getPage('/reqparams', method='POST', + headers=[("Content-Type", "multipart/form-data;boundary=X"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertStatus(400) + self.assertErrorPage(400, + "The request entity could not be decoded. The following charsets " + "were attempted: ['us-ascii', 'utf-8']") + + def test_nontext(self): + self.getPage('/nontext') + self.assertHeader('Content-Type', 'application/binary;charset=utf-8') + self.assertBody('\x00\x01\x02\x03') + + def testEncoding(self): + # Default encoding should be utf-8 + self.getPage('/mao_zedong') + self.assertBody(sing8) + + # Ask for utf-16. + self.getPage('/mao_zedong', [('Accept-Charset', 'utf-16')]) + self.assertHeader('Content-Type', 'text/html;charset=utf-16') + self.assertBody(sing16) + + # Ask for multiple encodings. ISO-8859-1 should fail, and utf-16 + # should be produced. + self.getPage('/mao_zedong', [('Accept-Charset', + 'iso-8859-1;q=1, utf-16;q=0.5')]) + self.assertBody(sing16) + + # The "*" value should default to our default_encoding, utf-8 + self.getPage('/mao_zedong', [('Accept-Charset', '*;q=1, utf-7;q=.2')]) + self.assertBody(sing8) + + # Only allow iso-8859-1, which should fail and raise 406. + self.getPage('/mao_zedong', [('Accept-Charset', 'iso-8859-1, *;q=0')]) + self.assertStatus("406 Not Acceptable") + self.assertInBody("Your client sent this Accept-Charset header: " + "iso-8859-1, *;q=0. We tried these charsets: " + "iso-8859-1.") + + # Ask for x-mac-ce, which should be unknown. See ticket #569. + self.getPage('/mao_zedong', [('Accept-Charset', + 'us-ascii, ISO-8859-1, x-mac-ce')]) + self.assertStatus("406 Not Acceptable") + self.assertInBody("Your client sent this Accept-Charset header: " + "us-ascii, ISO-8859-1, x-mac-ce. We tried these " + "charsets: ISO-8859-1, us-ascii, x-mac-ce.") + + # Test the 'encoding' arg to encode. + self.getPage('/utf8') + self.assertBody(sing8) + self.getPage('/utf8', [('Accept-Charset', 'us-ascii, ISO-8859-1')]) + self.assertStatus("406 Not Acceptable") + + def testGzip(self): + zbuf = BytesIO() + zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9) + zfile.write(ntob("Hello, world")) + zfile.close() + + self.getPage('/gzip/', headers=[("Accept-Encoding", "gzip")]) + self.assertInBody(zbuf.getvalue()[:3]) + self.assertHeader("Vary", "Accept-Encoding") + self.assertHeader("Content-Encoding", "gzip") + + # Test when gzip is denied. + self.getPage('/gzip/', headers=[("Accept-Encoding", "identity")]) + self.assertHeader("Vary", "Accept-Encoding") + self.assertNoHeader("Content-Encoding") + self.assertBody("Hello, world") + + self.getPage('/gzip/', headers=[("Accept-Encoding", "gzip;q=0")]) + self.assertHeader("Vary", "Accept-Encoding") + self.assertNoHeader("Content-Encoding") + self.assertBody("Hello, world") + + self.getPage('/gzip/', headers=[("Accept-Encoding", "*;q=0")]) + self.assertStatus(406) + self.assertNoHeader("Content-Encoding") + self.assertErrorPage(406, "identity, gzip") + + # Test for ticket #147 + self.getPage('/gzip/noshow', headers=[("Accept-Encoding", "gzip")]) + self.assertNoHeader('Content-Encoding') + self.assertStatus(500) + self.assertErrorPage(500, pattern="IndexError\n") + + # In this case, there's nothing we can do to deliver a + # readable page, since 1) the gzip header is already set, + # and 2) we may have already written some of the body. + # The fix is to never stream yields when using gzip. + if (cherrypy.server.protocol_version == "HTTP/1.0" or + getattr(cherrypy.server, "using_apache", False)): + self.getPage('/gzip/noshow_stream', + headers=[("Accept-Encoding", "gzip")]) + self.assertHeader('Content-Encoding', 'gzip') + self.assertInBody('\x1f\x8b\x08\x00') + else: + # The wsgiserver will simply stop sending data, and the HTTP client + # will error due to an incomplete chunk-encoded stream. + self.assertRaises((ValueError, IncompleteRead), self.getPage, + '/gzip/noshow_stream', + headers=[("Accept-Encoding", "gzip")]) + + def test_UnicodeHeaders(self): + self.getPage('/cookies_and_headers') + self.assertBody('Any content') + diff --git a/libs/cherrypy/test/test_etags.py b/libs/cherrypy/test/test_etags.py new file mode 100644 index 0000000..aec1693 --- /dev/null +++ b/libs/cherrypy/test/test_etags.py @@ -0,0 +1,83 @@ +import cherrypy +from cherrypy._cpcompat import ntou +from cherrypy.test import helper + + +class ETagTest(helper.CPWebCase): + + def setup_server(): + class Root: + def resource(self): + return "Oh wah ta goo Siam." + resource.exposed = True + + def fail(self, code): + code = int(code) + if 300 <= code <= 399: + raise cherrypy.HTTPRedirect([], code) + else: + raise cherrypy.HTTPError(code) + fail.exposed = True + + def unicoded(self): + return ntou('I am a \u1ee4nicode string.', 'escape') + unicoded.exposed = True + # In Python 3, tools.encode is on by default + unicoded._cp_config = {'tools.encode.on': True} + + conf = {'/': {'tools.etags.on': True, + 'tools.etags.autotags': True, + }} + cherrypy.tree.mount(Root(), config=conf) + setup_server = staticmethod(setup_server) + + def test_etags(self): + self.getPage("/resource") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.assertBody('Oh wah ta goo Siam.') + etag = self.assertHeader('ETag') + + # Test If-Match (both valid and invalid) + self.getPage("/resource", headers=[('If-Match', etag)]) + self.assertStatus("200 OK") + self.getPage("/resource", headers=[('If-Match', "*")]) + self.assertStatus("200 OK") + self.getPage("/resource", headers=[('If-Match', "*")], method="POST") + self.assertStatus("200 OK") + self.getPage("/resource", headers=[('If-Match', "a bogus tag")]) + self.assertStatus("412 Precondition Failed") + + # Test If-None-Match (both valid and invalid) + self.getPage("/resource", headers=[('If-None-Match', etag)]) + self.assertStatus(304) + self.getPage("/resource", method='POST', headers=[('If-None-Match', etag)]) + self.assertStatus("412 Precondition Failed") + self.getPage("/resource", headers=[('If-None-Match', "*")]) + self.assertStatus(304) + self.getPage("/resource", headers=[('If-None-Match', "a bogus tag")]) + self.assertStatus("200 OK") + + def test_errors(self): + self.getPage("/resource") + self.assertStatus(200) + etag = self.assertHeader('ETag') + + # Test raising errors in page handler + self.getPage("/fail/412", headers=[('If-Match', etag)]) + self.assertStatus(412) + self.getPage("/fail/304", headers=[('If-Match', etag)]) + self.assertStatus(304) + self.getPage("/fail/412", headers=[('If-None-Match', "*")]) + self.assertStatus(412) + self.getPage("/fail/304", headers=[('If-None-Match', "*")]) + self.assertStatus(304) + + def test_unicode_body(self): + self.getPage("/unicoded") + self.assertStatus(200) + etag1 = self.assertHeader('ETag') + self.getPage("/unicoded", headers=[('If-Match', etag1)]) + self.assertStatus(200) + self.assertHeader('ETag', etag1) + diff --git a/libs/cherrypy/test/test_http.py b/libs/cherrypy/test/test_http.py new file mode 100644 index 0000000..639c6c4 --- /dev/null +++ b/libs/cherrypy/test/test_http.py @@ -0,0 +1,212 @@ +"""Tests for managing HTTP issues (malformed requests, etc).""" + +import errno +import mimetypes +import socket +import sys + +import cherrypy +from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob, py3k + + +def encode_multipart_formdata(files): + """Return (content_type, body) ready for httplib.HTTP instance. + + files: a sequence of (name, filename, value) tuples for multipart uploads. + """ + BOUNDARY = '________ThIs_Is_tHe_bouNdaRY_$' + L = [] + for key, filename, value in files: + L.append('--' + BOUNDARY) + L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % + (key, filename)) + ct = mimetypes.guess_type(filename)[0] or 'application/octet-stream' + L.append('Content-Type: %s' % ct) + L.append('') + L.append(value) + L.append('--' + BOUNDARY + '--') + L.append('') + body = '\r\n'.join(L) + content_type = 'multipart/form-data; boundary=%s' % BOUNDARY + return content_type, body + + + + +from cherrypy.test import helper + +class HTTPTests(helper.CPWebCase): + + def setup_server(): + class Root: + def index(self, *args, **kwargs): + return "Hello world!" + index.exposed = True + + def no_body(self, *args, **kwargs): + return "Hello world!" + no_body.exposed = True + no_body._cp_config = {'request.process_request_body': False} + + def post_multipart(self, file): + """Return a summary ("a * 65536\nb * 65536") of the uploaded file.""" + contents = file.file.read() + summary = [] + curchar = None + count = 0 + for c in contents: + if c == curchar: + count += 1 + else: + if count: + if py3k: curchar = chr(curchar) + summary.append("%s * %d" % (curchar, count)) + count = 1 + curchar = c + if count: + if py3k: curchar = chr(curchar) + summary.append("%s * %d" % (curchar, count)) + return ", ".join(summary) + post_multipart.exposed = True + + cherrypy.tree.mount(Root()) + cherrypy.config.update({'server.max_request_body_size': 30000000}) + setup_server = staticmethod(setup_server) + + def test_no_content_length(self): + # "The presence of a message-body in a request is signaled by the + # inclusion of a Content-Length or Transfer-Encoding header field in + # the request's message-headers." + # + # Send a message with neither header and no body. Even though + # the request is of method POST, this should be OK because we set + # request.process_request_body to False for our handler. + if self.scheme == "https": + c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) + else: + c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + c.request("POST", "/no_body") + response = c.getresponse() + self.body = response.fp.read() + self.status = str(response.status) + self.assertStatus(200) + self.assertBody(ntob('Hello world!')) + + # Now send a message that has no Content-Length, but does send a body. + # Verify that CP times out the socket and responds + # with 411 Length Required. + if self.scheme == "https": + c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) + else: + c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + c.request("POST", "/") + response = c.getresponse() + self.body = response.fp.read() + self.status = str(response.status) + self.assertStatus(411) + + def test_post_multipart(self): + alphabet = "abcdefghijklmnopqrstuvwxyz" + # generate file contents for a large post + contents = "".join([c * 65536 for c in alphabet]) + + # encode as multipart form data + files=[('file', 'file.txt', contents)] + content_type, body = encode_multipart_formdata(files) + body = body.encode('Latin-1') + + # post file + if self.scheme == 'https': + c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) + else: + c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + c.putrequest('POST', '/post_multipart') + c.putheader('Content-Type', content_type) + c.putheader('Content-Length', str(len(body))) + c.endheaders() + c.send(body) + + response = c.getresponse() + self.body = response.fp.read() + self.status = str(response.status) + self.assertStatus(200) + self.assertBody(", ".join(["%s * 65536" % c for c in alphabet])) + + def test_malformed_request_line(self): + if getattr(cherrypy.server, "using_apache", False): + return self.skip("skipped due to known Apache differences...") + + # Test missing version in Request-Line + if self.scheme == 'https': + c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) + else: + c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + c._output(ntob('GET /')) + c._send_output() + if hasattr(c, 'strict'): + response = c.response_class(c.sock, strict=c.strict, method='GET') + else: + # Python 3.2 removed the 'strict' feature, saying: + # "http.client now always assumes HTTP/1.x compliant servers." + response = c.response_class(c.sock, method='GET') + response.begin() + self.assertEqual(response.status, 400) + self.assertEqual(response.fp.read(22), ntob("Malformed Request-Line")) + c.close() + + def test_malformed_header(self): + if self.scheme == 'https': + c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) + else: + c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + c.putrequest('GET', '/') + c.putheader('Content-Type', 'text/plain') + # See http://www.cherrypy.org/ticket/941 + c._output(ntob('Re, 1.2.3.4#015#012')) + c.endheaders() + + response = c.getresponse() + self.status = str(response.status) + self.assertStatus(400) + self.body = response.fp.read(20) + self.assertBody("Illegal header line.") + + def test_http_over_https(self): + if self.scheme != 'https': + return self.skip("skipped (not running HTTPS)... ") + + # Try connecting without SSL. + conn = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + conn.putrequest("GET", "/", skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method="GET") + try: + response.begin() + self.assertEqual(response.status, 400) + self.body = response.read() + self.assertBody("The client sent a plain HTTP request, but this " + "server only speaks HTTPS on this port.") + except socket.error: + e = sys.exc_info()[1] + # "Connection reset by peer" is also acceptable. + if e.errno != errno.ECONNRESET: + raise + + def test_garbage_in(self): + # Connect without SSL regardless of server.scheme + c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + c._output(ntob('gjkgjklsgjklsgjkljklsg')) + c._send_output() + response = c.response_class(c.sock, method="GET") + try: + response.begin() + self.assertEqual(response.status, 400) + self.assertEqual(response.fp.read(22), ntob("Malformed Request-Line")) + c.close() + except socket.error: + e = sys.exc_info()[1] + # "Connection reset by peer" is also acceptable. + if e.errno != errno.ECONNRESET: + raise + diff --git a/libs/cherrypy/test/test_httpauth.py b/libs/cherrypy/test/test_httpauth.py new file mode 100644 index 0000000..9d0eecb --- /dev/null +++ b/libs/cherrypy/test/test_httpauth.py @@ -0,0 +1,151 @@ +import cherrypy +from cherrypy._cpcompat import md5, sha, ntob +from cherrypy.lib import httpauth + +from cherrypy.test import helper + +class HTTPAuthTest(helper.CPWebCase): + + def setup_server(): + class Root: + def index(self): + return "This is public." + index.exposed = True + + class DigestProtected: + def index(self): + return "Hello %s, you've been authorized." % cherrypy.request.login + index.exposed = True + + class BasicProtected: + def index(self): + return "Hello %s, you've been authorized." % cherrypy.request.login + index.exposed = True + + class BasicProtected2: + def index(self): + return "Hello %s, you've been authorized." % cherrypy.request.login + index.exposed = True + + def fetch_users(): + return {'test': 'test'} + + def sha_password_encrypter(password): + return sha(ntob(password)).hexdigest() + + def fetch_password(username): + return sha(ntob('test')).hexdigest() + + conf = {'/digest': {'tools.digest_auth.on': True, + 'tools.digest_auth.realm': 'localhost', + 'tools.digest_auth.users': fetch_users}, + '/basic': {'tools.basic_auth.on': True, + 'tools.basic_auth.realm': 'localhost', + 'tools.basic_auth.users': {'test': md5(ntob('test')).hexdigest()}}, + '/basic2': {'tools.basic_auth.on': True, + 'tools.basic_auth.realm': 'localhost', + 'tools.basic_auth.users': fetch_password, + 'tools.basic_auth.encrypt': sha_password_encrypter}} + + root = Root() + root.digest = DigestProtected() + root.basic = BasicProtected() + root.basic2 = BasicProtected2() + cherrypy.tree.mount(root, config=conf) + setup_server = staticmethod(setup_server) + + + def testPublic(self): + self.getPage("/") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.assertBody('This is public.') + + def testBasic(self): + self.getPage("/basic/") + self.assertStatus(401) + self.assertHeader('WWW-Authenticate', 'Basic realm="localhost"') + + self.getPage('/basic/', [('Authorization', 'Basic dGVzdDp0ZX60')]) + self.assertStatus(401) + + self.getPage('/basic/', [('Authorization', 'Basic dGVzdDp0ZXN0')]) + self.assertStatus('200 OK') + self.assertBody("Hello test, you've been authorized.") + + def testBasic2(self): + self.getPage("/basic2/") + self.assertStatus(401) + self.assertHeader('WWW-Authenticate', 'Basic realm="localhost"') + + self.getPage('/basic2/', [('Authorization', 'Basic dGVzdDp0ZX60')]) + self.assertStatus(401) + + self.getPage('/basic2/', [('Authorization', 'Basic dGVzdDp0ZXN0')]) + self.assertStatus('200 OK') + self.assertBody("Hello test, you've been authorized.") + + def testDigest(self): + self.getPage("/digest/") + self.assertStatus(401) + + value = None + for k, v in self.headers: + if k.lower() == "www-authenticate": + if v.startswith("Digest"): + value = v + break + + if value is None: + self._handlewebError("Digest authentification scheme was not found") + + value = value[7:] + items = value.split(', ') + tokens = {} + for item in items: + key, value = item.split('=') + tokens[key.lower()] = value + + missing_msg = "%s is missing" + bad_value_msg = "'%s' was expecting '%s' but found '%s'" + nonce = None + if 'realm' not in tokens: + self._handlewebError(missing_msg % 'realm') + elif tokens['realm'] != '"localhost"': + self._handlewebError(bad_value_msg % ('realm', '"localhost"', tokens['realm'])) + if 'nonce' not in tokens: + self._handlewebError(missing_msg % 'nonce') + else: + nonce = tokens['nonce'].strip('"') + if 'algorithm' not in tokens: + self._handlewebError(missing_msg % 'algorithm') + elif tokens['algorithm'] != '"MD5"': + self._handlewebError(bad_value_msg % ('algorithm', '"MD5"', tokens['algorithm'])) + if 'qop' not in tokens: + self._handlewebError(missing_msg % 'qop') + elif tokens['qop'] != '"auth"': + self._handlewebError(bad_value_msg % ('qop', '"auth"', tokens['qop'])) + + # Test a wrong 'realm' value + base_auth = 'Digest username="test", realm="wrong realm", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"' + + auth = base_auth % (nonce, '', '00000001') + params = httpauth.parseAuthorization(auth) + response = httpauth._computeDigestResponse(params, 'test') + + auth = base_auth % (nonce, response, '00000001') + self.getPage('/digest/', [('Authorization', auth)]) + self.assertStatus(401) + + # Test that must pass + base_auth = 'Digest username="test", realm="localhost", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"' + + auth = base_auth % (nonce, '', '00000001') + params = httpauth.parseAuthorization(auth) + response = httpauth._computeDigestResponse(params, 'test') + + auth = base_auth % (nonce, response, '00000001') + self.getPage('/digest/', [('Authorization', auth)]) + self.assertStatus('200 OK') + self.assertBody("Hello test, you've been authorized.") + diff --git a/libs/cherrypy/test/test_httplib.py b/libs/cherrypy/test/test_httplib.py new file mode 100644 index 0000000..5dc40fd --- /dev/null +++ b/libs/cherrypy/test/test_httplib.py @@ -0,0 +1,29 @@ +"""Tests for cherrypy/lib/httputil.py.""" + +import unittest +from cherrypy.lib import httputil + + +class UtilityTests(unittest.TestCase): + + def test_urljoin(self): + # Test all slash+atom combinations for SCRIPT_NAME and PATH_INFO + self.assertEqual(httputil.urljoin("/sn/", "/pi/"), "/sn/pi/") + self.assertEqual(httputil.urljoin("/sn/", "/pi"), "/sn/pi") + self.assertEqual(httputil.urljoin("/sn/", "/"), "/sn/") + self.assertEqual(httputil.urljoin("/sn/", ""), "/sn/") + self.assertEqual(httputil.urljoin("/sn", "/pi/"), "/sn/pi/") + self.assertEqual(httputil.urljoin("/sn", "/pi"), "/sn/pi") + self.assertEqual(httputil.urljoin("/sn", "/"), "/sn/") + self.assertEqual(httputil.urljoin("/sn", ""), "/sn") + self.assertEqual(httputil.urljoin("/", "/pi/"), "/pi/") + self.assertEqual(httputil.urljoin("/", "/pi"), "/pi") + self.assertEqual(httputil.urljoin("/", "/"), "/") + self.assertEqual(httputil.urljoin("/", ""), "/") + self.assertEqual(httputil.urljoin("", "/pi/"), "/pi/") + self.assertEqual(httputil.urljoin("", "/pi"), "/pi") + self.assertEqual(httputil.urljoin("", "/"), "/") + self.assertEqual(httputil.urljoin("", ""), "/") + +if __name__ == '__main__': + unittest.main() diff --git a/libs/cherrypy/test/test_json.py b/libs/cherrypy/test/test_json.py new file mode 100644 index 0000000..a02c076 --- /dev/null +++ b/libs/cherrypy/test/test_json.py @@ -0,0 +1,79 @@ +import cherrypy +from cherrypy.test import helper + +from cherrypy._cpcompat import json + +class JsonTest(helper.CPWebCase): + def setup_server(): + class Root(object): + def plain(self): + return 'hello' + plain.exposed = True + + def json_string(self): + return 'hello' + json_string.exposed = True + json_string._cp_config = {'tools.json_out.on': True} + + def json_list(self): + return ['a', 'b', 42] + json_list.exposed = True + json_list._cp_config = {'tools.json_out.on': True} + + def json_dict(self): + return {'answer': 42} + json_dict.exposed = True + json_dict._cp_config = {'tools.json_out.on': True} + + def json_post(self): + if cherrypy.request.json == [13, 'c']: + return 'ok' + else: + return 'nok' + json_post.exposed = True + json_post._cp_config = {'tools.json_in.on': True} + + root = Root() + cherrypy.tree.mount(root) + setup_server = staticmethod(setup_server) + + def test_json_output(self): + if json is None: + self.skip("json not found ") + return + + self.getPage("/plain") + self.assertBody("hello") + + self.getPage("/json_string") + self.assertBody('"hello"') + + self.getPage("/json_list") + self.assertBody('["a", "b", 42]') + + self.getPage("/json_dict") + self.assertBody('{"answer": 42}') + + def test_json_input(self): + if json is None: + self.skip("json not found ") + return + + body = '[13, "c"]' + headers = [('Content-Type', 'application/json'), + ('Content-Length', str(len(body)))] + self.getPage("/json_post", method="POST", headers=headers, body=body) + self.assertBody('ok') + + body = '[13, "c"]' + headers = [('Content-Type', 'text/plain'), + ('Content-Length', str(len(body)))] + self.getPage("/json_post", method="POST", headers=headers, body=body) + self.assertStatus(415, 'Expected an application/json content type') + + body = '[13, -]' + headers = [('Content-Type', 'application/json'), + ('Content-Length', str(len(body)))] + self.getPage("/json_post", method="POST", headers=headers, body=body) + self.assertStatus(400, 'Invalid JSON document') + diff --git a/libs/cherrypy/test/test_logging.py b/libs/cherrypy/test/test_logging.py new file mode 100644 index 0000000..7d506e8 --- /dev/null +++ b/libs/cherrypy/test/test_logging.py @@ -0,0 +1,157 @@ +"""Basic tests for the CherryPy core: request handling.""" + +import os +localDir = os.path.dirname(__file__) + +import cherrypy +from cherrypy._cpcompat import ntob, ntou, py3k + +access_log = os.path.join(localDir, "access.log") +error_log = os.path.join(localDir, "error.log") + +# Some unicode strings. +tartaros = ntou('\u03a4\u1f71\u03c1\u03c4\u03b1\u03c1\u03bf\u03c2', 'escape') +erebos = ntou('\u0388\u03c1\u03b5\u03b2\u03bf\u03c2.com', 'escape') + + +def setup_server(): + class Root: + + def index(self): + return "hello" + index.exposed = True + + def uni_code(self): + cherrypy.request.login = tartaros + cherrypy.request.remote.name = erebos + uni_code.exposed = True + + def slashes(self): + cherrypy.request.request_line = r'GET /slashed\path HTTP/1.1' + slashes.exposed = True + + def whitespace(self): + # User-Agent = "User-Agent" ":" 1*( product | comment ) + # comment = "(" *( ctext | quoted-pair | comment ) ")" + # ctext = + # TEXT = + # LWS = [CRLF] 1*( SP | HT ) + cherrypy.request.headers['User-Agent'] = 'Browzuh (1.0\r\n\t\t.3)' + whitespace.exposed = True + + def as_string(self): + return "content" + as_string.exposed = True + + def as_yield(self): + yield "content" + as_yield.exposed = True + + def error(self): + raise ValueError() + error.exposed = True + error._cp_config = {'tools.log_tracebacks.on': True} + + root = Root() + + + cherrypy.config.update({'log.error_file': error_log, + 'log.access_file': access_log, + }) + cherrypy.tree.mount(root) + + + +from cherrypy.test import helper, logtest + +class AccessLogTests(helper.CPWebCase, logtest.LogCase): + setup_server = staticmethod(setup_server) + + logfile = access_log + + def testNormalReturn(self): + self.markLog() + self.getPage("/as_string", + headers=[('Referer', 'http://www.cherrypy.org/'), + ('User-Agent', 'Mozilla/5.0')]) + self.assertBody('content') + self.assertStatus(200) + + intro = '%s - - [' % self.interface() + + self.assertLog(-1, intro) + + if [k for k, v in self.headers if k.lower() == 'content-length']: + self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 7 ' + '"http://www.cherrypy.org/" "Mozilla/5.0"' + % self.prefix()) + else: + self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 - ' + '"http://www.cherrypy.org/" "Mozilla/5.0"' + % self.prefix()) + + def testNormalYield(self): + self.markLog() + self.getPage("/as_yield") + self.assertBody('content') + self.assertStatus(200) + + intro = '%s - - [' % self.interface() + + self.assertLog(-1, intro) + if [k for k, v in self.headers if k.lower() == 'content-length']: + self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 7 "" ""' % + self.prefix()) + else: + self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 - "" ""' + % self.prefix()) + + def testEscapedOutput(self): + # Test unicode in access log pieces. + self.markLog() + self.getPage("/uni_code") + self.assertStatus(200) + if py3k: + # The repr of a bytestring in py3k includes a b'' prefix + self.assertLog(-1, repr(tartaros.encode('utf8'))[2:-1]) + else: + self.assertLog(-1, repr(tartaros.encode('utf8'))[1:-1]) + # Test the erebos value. Included inline for your enlightenment. + # Note the 'r' prefix--those backslashes are literals. + self.assertLog(-1, r'\xce\x88\xcf\x81\xce\xb5\xce\xb2\xce\xbf\xcf\x82') + + # Test backslashes in output. + self.markLog() + self.getPage("/slashes") + self.assertStatus(200) + if py3k: + self.assertLog(-1, ntob('"GET /slashed\\path HTTP/1.1"')) + else: + self.assertLog(-1, r'"GET /slashed\\path HTTP/1.1"') + + # Test whitespace in output. + self.markLog() + self.getPage("/whitespace") + self.assertStatus(200) + # Again, note the 'r' prefix. + self.assertLog(-1, r'"Browzuh (1.0\r\n\t\t.3)"') + + +class ErrorLogTests(helper.CPWebCase, logtest.LogCase): + setup_server = staticmethod(setup_server) + + logfile = error_log + + def testTracebacks(self): + # Test that tracebacks get written to the error log. + self.markLog() + ignore = helper.webtest.ignored_exceptions + ignore.append(ValueError) + try: + self.getPage("/error") + self.assertInBody("raise ValueError()") + self.assertLog(0, 'HTTP Traceback (most recent call last):') + self.assertLog(-3, 'raise ValueError()') + finally: + ignore.pop() + diff --git a/libs/cherrypy/test/test_mime.py b/libs/cherrypy/test/test_mime.py new file mode 100644 index 0000000..1605991 --- /dev/null +++ b/libs/cherrypy/test/test_mime.py @@ -0,0 +1,128 @@ +"""Tests for various MIME issues, including the safe_multipart Tool.""" + +import cherrypy +from cherrypy._cpcompat import ntob, ntou, sorted + +def setup_server(): + + class Root: + + def multipart(self, parts): + return repr(parts) + multipart.exposed = True + + def multipart_form_data(self, **kwargs): + return repr(list(sorted(kwargs.items()))) + multipart_form_data.exposed = True + + def flashupload(self, Filedata, Upload, Filename): + return ("Upload: %s, Filename: %s, Filedata: %r" % + (Upload, Filename, Filedata.file.read())) + flashupload.exposed = True + + cherrypy.config.update({'server.max_request_body_size': 0}) + cherrypy.tree.mount(Root()) + + +# Client-side code # + +from cherrypy.test import helper + +class MultipartTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_multipart(self): + text_part = ntou("This is the text version") + html_part = ntou(""" + + + + + + +This is the HTML version + + +""") + body = '\r\n'.join([ + "--123456789", + "Content-Type: text/plain; charset='ISO-8859-1'", + "Content-Transfer-Encoding: 7bit", + "", + text_part, + "--123456789", + "Content-Type: text/html; charset='ISO-8859-1'", + "", + html_part, + "--123456789--"]) + headers = [ + ('Content-Type', 'multipart/mixed; boundary=123456789'), + ('Content-Length', str(len(body))), + ] + self.getPage('/multipart', headers, "POST", body) + self.assertBody(repr([text_part, html_part])) + + def test_multipart_form_data(self): + body='\r\n'.join(['--X', + 'Content-Disposition: form-data; name="foo"', + '', + 'bar', + '--X', + # Test a param with more than one value. + # See http://www.cherrypy.org/ticket/1028 + 'Content-Disposition: form-data; name="baz"', + '', + '111', + '--X', + 'Content-Disposition: form-data; name="baz"', + '', + '333', + '--X--']) + self.getPage('/multipart_form_data', method='POST', + headers=[("Content-Type", "multipart/form-data;boundary=X"), + ("Content-Length", str(len(body))), + ], + body=body), + self.assertBody(repr([('baz', [ntou('111'), ntou('333')]), ('foo', ntou('bar'))])) + + +class SafeMultipartHandlingTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_Flash_Upload(self): + headers = [ + ('Accept', 'text/*'), + ('Content-Type', 'multipart/form-data; ' + 'boundary=----------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6'), + ('User-Agent', 'Shockwave Flash'), + ('Host', 'www.example.com:54583'), + ('Content-Length', '499'), + ('Connection', 'Keep-Alive'), + ('Cache-Control', 'no-cache'), + ] + filedata = ntob('\r\n' + '\r\n' + '\r\n') + body = (ntob( + '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n' + 'Content-Disposition: form-data; name="Filename"\r\n' + '\r\n' + '.project\r\n' + '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n' + 'Content-Disposition: form-data; ' + 'name="Filedata"; filename=".project"\r\n' + 'Content-Type: application/octet-stream\r\n' + '\r\n') + + filedata + + ntob('\r\n' + '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n' + 'Content-Disposition: form-data; name="Upload"\r\n' + '\r\n' + 'Submit Query\r\n' + # Flash apps omit the trailing \r\n on the last line: + '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6--' + )) + self.getPage('/flashupload', headers, "POST", body) + self.assertBody("Upload: Submit Query, Filename: .project, " + "Filedata: %r" % filedata) + diff --git a/libs/cherrypy/test/test_misc_tools.py b/libs/cherrypy/test/test_misc_tools.py new file mode 100644 index 0000000..1dd1429 --- /dev/null +++ b/libs/cherrypy/test/test_misc_tools.py @@ -0,0 +1,207 @@ +import os +localDir = os.path.dirname(__file__) +logfile = os.path.join(localDir, "test_misc_tools.log") + +import cherrypy +from cherrypy import tools + + +def setup_server(): + class Root: + def index(self): + yield "Hello, world" + index.exposed = True + h = [("Content-Language", "en-GB"), ('Content-Type', 'text/plain')] + tools.response_headers(headers=h)(index) + + def other(self): + return "salut" + other.exposed = True + other._cp_config = { + 'tools.response_headers.on': True, + 'tools.response_headers.headers': [("Content-Language", "fr"), + ('Content-Type', 'text/plain')], + 'tools.log_hooks.on': True, + } + + + class Accept: + _cp_config = {'tools.accept.on': True} + + def index(self): + return 'Atom feed' + index.exposed = True + + # In Python 2.4+, we could use a decorator instead: + # @tools.accept('application/atom+xml') + def feed(self): + return """ + + Unknown Blog +""" + feed.exposed = True + feed._cp_config = {'tools.accept.media': 'application/atom+xml'} + + def select(self): + # We could also write this: mtype = cherrypy.lib.accept.accept(...) + mtype = tools.accept.callable(['text/html', 'text/plain']) + if mtype == 'text/html': + return "

Page Title

" + else: + return "PAGE TITLE" + select.exposed = True + + class Referer: + def accept(self): + return "Accepted!" + accept.exposed = True + reject = accept + + class AutoVary: + def index(self): + # Read a header directly with 'get' + ae = cherrypy.request.headers.get('Accept-Encoding') + # Read a header directly with '__getitem__' + cl = cherrypy.request.headers['Host'] + # Read a header directly with '__contains__' + hasif = 'If-Modified-Since' in cherrypy.request.headers + # Read a header directly with 'has_key' + if hasattr(dict, 'has_key'): + # Python 2 + has = cherrypy.request.headers.has_key('Range') + else: + # Python 3 + has = 'Range' in cherrypy.request.headers + # Call a lib function + mtype = tools.accept.callable(['text/html', 'text/plain']) + return "Hello, world!" + index.exposed = True + + conf = {'/referer': {'tools.referer.on': True, + 'tools.referer.pattern': r'http://[^/]*example\.com', + }, + '/referer/reject': {'tools.referer.accept': False, + 'tools.referer.accept_missing': True, + }, + '/autovary': {'tools.autovary.on': True}, + } + + root = Root() + root.referer = Referer() + root.accept = Accept() + root.autovary = AutoVary() + cherrypy.tree.mount(root, config=conf) + cherrypy.config.update({'log.error_file': logfile}) + + +from cherrypy.test import helper + +class ResponseHeadersTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def testResponseHeadersDecorator(self): + self.getPage('/') + self.assertHeader("Content-Language", "en-GB") + self.assertHeader('Content-Type', 'text/plain;charset=utf-8') + + def testResponseHeaders(self): + self.getPage('/other') + self.assertHeader("Content-Language", "fr") + self.assertHeader('Content-Type', 'text/plain;charset=utf-8') + + +class RefererTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def testReferer(self): + self.getPage('/referer/accept') + self.assertErrorPage(403, 'Forbidden Referer header.') + + self.getPage('/referer/accept', + headers=[('Referer', 'http://www.example.com/')]) + self.assertStatus(200) + self.assertBody('Accepted!') + + # Reject + self.getPage('/referer/reject') + self.assertStatus(200) + self.assertBody('Accepted!') + + self.getPage('/referer/reject', + headers=[('Referer', 'http://www.example.com/')]) + self.assertErrorPage(403, 'Forbidden Referer header.') + + +class AcceptTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_Accept_Tool(self): + # Test with no header provided + self.getPage('/accept/feed') + self.assertStatus(200) + self.assertInBody('Unknown Blog') + + # Specify exact media type + self.getPage('/accept/feed', headers=[('Accept', 'application/atom+xml')]) + self.assertStatus(200) + self.assertInBody('Unknown Blog') + + # Specify matching media range + self.getPage('/accept/feed', headers=[('Accept', 'application/*')]) + self.assertStatus(200) + self.assertInBody('Unknown Blog') + + # Specify all media ranges + self.getPage('/accept/feed', headers=[('Accept', '*/*')]) + self.assertStatus(200) + self.assertInBody('Unknown Blog') + + # Specify unacceptable media types + self.getPage('/accept/feed', headers=[('Accept', 'text/html')]) + self.assertErrorPage(406, + "Your client sent this Accept header: text/html. " + "But this resource only emits these media types: " + "application/atom+xml.") + + # Test resource where tool is 'on' but media is None (not set). + self.getPage('/accept/') + self.assertStatus(200) + self.assertBody('Atom feed') + + def test_accept_selection(self): + # Try both our expected media types + self.getPage('/accept/select', [('Accept', 'text/html')]) + self.assertStatus(200) + self.assertBody('

Page Title

') + self.getPage('/accept/select', [('Accept', 'text/plain')]) + self.assertStatus(200) + self.assertBody('PAGE TITLE') + self.getPage('/accept/select', [('Accept', 'text/plain, text/*;q=0.5')]) + self.assertStatus(200) + self.assertBody('PAGE TITLE') + + # text/* and */* should prefer text/html since it comes first + # in our 'media' argument to tools.accept + self.getPage('/accept/select', [('Accept', 'text/*')]) + self.assertStatus(200) + self.assertBody('

Page Title

') + self.getPage('/accept/select', [('Accept', '*/*')]) + self.assertStatus(200) + self.assertBody('

Page Title

') + + # Try unacceptable media types + self.getPage('/accept/select', [('Accept', 'application/xml')]) + self.assertErrorPage(406, + "Your client sent this Accept header: application/xml. " + "But this resource only emits these media types: " + "text/html, text/plain.") + + +class AutoVaryTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def testAutoVary(self): + self.getPage('/autovary/') + self.assertHeader( + "Vary", 'Accept, Accept-Charset, Accept-Encoding, Host, If-Modified-Since, Range') + diff --git a/libs/cherrypy/test/test_objectmapping.py b/libs/cherrypy/test/test_objectmapping.py new file mode 100644 index 0000000..8dcf2d3 --- /dev/null +++ b/libs/cherrypy/test/test_objectmapping.py @@ -0,0 +1,404 @@ +import cherrypy +from cherrypy._cpcompat import ntou +from cherrypy._cptree import Application +from cherrypy.test import helper + +script_names = ["", "/foo", "/users/fred/blog", "/corp/blog"] + + +class ObjectMappingTest(helper.CPWebCase): + + def setup_server(): + class Root: + def index(self, name="world"): + return name + index.exposed = True + + def foobar(self): + return "bar" + foobar.exposed = True + + def default(self, *params, **kwargs): + return "default:" + repr(params) + default.exposed = True + + def other(self): + return "other" + other.exposed = True + + def extra(self, *p): + return repr(p) + extra.exposed = True + + def redirect(self): + raise cherrypy.HTTPRedirect('dir1/', 302) + redirect.exposed = True + + def notExposed(self): + return "not exposed" + + def confvalue(self): + return cherrypy.request.config.get("user") + confvalue.exposed = True + + def redirect_via_url(self, path): + raise cherrypy.HTTPRedirect(cherrypy.url(path)) + redirect_via_url.exposed = True + + def translate_html(self): + return "OK" + translate_html.exposed = True + + def mapped_func(self, ID=None): + return "ID is %s" % ID + mapped_func.exposed = True + setattr(Root, "Von B\xfclow", mapped_func) + + + class Exposing: + def base(self): + return "expose works!" + cherrypy.expose(base) + cherrypy.expose(base, "1") + cherrypy.expose(base, "2") + + class ExposingNewStyle(object): + def base(self): + return "expose works!" + cherrypy.expose(base) + cherrypy.expose(base, "1") + cherrypy.expose(base, "2") + + + class Dir1: + def index(self): + return "index for dir1" + index.exposed = True + + def myMethod(self): + return "myMethod from dir1, path_info is:" + repr(cherrypy.request.path_info) + myMethod.exposed = True + myMethod._cp_config = {'tools.trailing_slash.extra': True} + + def default(self, *params): + return "default for dir1, param is:" + repr(params) + default.exposed = True + + + class Dir2: + def index(self): + return "index for dir2, path is:" + cherrypy.request.path_info + index.exposed = True + + def script_name(self): + return cherrypy.tree.script_name() + script_name.exposed = True + + def cherrypy_url(self): + return cherrypy.url("/extra") + cherrypy_url.exposed = True + + def posparam(self, *vpath): + return "/".join(vpath) + posparam.exposed = True + + + class Dir3: + def default(self): + return "default for dir3, not exposed" + + class Dir4: + def index(self): + return "index for dir4, not exposed" + + class DefNoIndex: + def default(self, *args): + raise cherrypy.HTTPRedirect("contact") + default.exposed = True + + # MethodDispatcher code + class ByMethod: + exposed = True + + def __init__(self, *things): + self.things = list(things) + + def GET(self): + return repr(self.things) + + def POST(self, thing): + self.things.append(thing) + + class Collection: + default = ByMethod('a', 'bit') + + Root.exposing = Exposing() + Root.exposingnew = ExposingNewStyle() + Root.dir1 = Dir1() + Root.dir1.dir2 = Dir2() + Root.dir1.dir2.dir3 = Dir3() + Root.dir1.dir2.dir3.dir4 = Dir4() + Root.defnoindex = DefNoIndex() + Root.bymethod = ByMethod('another') + Root.collection = Collection() + + d = cherrypy.dispatch.MethodDispatcher() + for url in script_names: + conf = {'/': {'user': (url or "/").split("/")[-2]}, + '/bymethod': {'request.dispatch': d}, + '/collection': {'request.dispatch': d}, + } + cherrypy.tree.mount(Root(), url, conf) + + + class Isolated: + def index(self): + return "made it!" + index.exposed = True + + cherrypy.tree.mount(Isolated(), "/isolated") + + class AnotherApp: + + exposed = True + + def GET(self): + return "milk" + + cherrypy.tree.mount(AnotherApp(), "/app", {'/': {'request.dispatch': d}}) + setup_server = staticmethod(setup_server) + + + def testObjectMapping(self): + for url in script_names: + prefix = self.script_name = url + + self.getPage('/') + self.assertBody('world') + + self.getPage("/dir1/myMethod") + self.assertBody("myMethod from dir1, path_info is:'/dir1/myMethod'") + + self.getPage("/this/method/does/not/exist") + self.assertBody("default:('this', 'method', 'does', 'not', 'exist')") + + self.getPage("/extra/too/much") + self.assertBody("('too', 'much')") + + self.getPage("/other") + self.assertBody('other') + + self.getPage("/notExposed") + self.assertBody("default:('notExposed',)") + + self.getPage("/dir1/dir2/") + self.assertBody('index for dir2, path is:/dir1/dir2/') + + # Test omitted trailing slash (should be redirected by default). + self.getPage("/dir1/dir2") + self.assertStatus(301) + self.assertHeader('Location', '%s/dir1/dir2/' % self.base()) + + # Test extra trailing slash (should be redirected if configured). + self.getPage("/dir1/myMethod/") + self.assertStatus(301) + self.assertHeader('Location', '%s/dir1/myMethod' % self.base()) + + # Test that default method must be exposed in order to match. + self.getPage("/dir1/dir2/dir3/dir4/index") + self.assertBody("default for dir1, param is:('dir2', 'dir3', 'dir4', 'index')") + + # Test *vpath when default() is defined but not index() + # This also tests HTTPRedirect with default. + self.getPage("/defnoindex") + self.assertStatus((302, 303)) + self.assertHeader('Location', '%s/contact' % self.base()) + self.getPage("/defnoindex/") + self.assertStatus((302, 303)) + self.assertHeader('Location', '%s/defnoindex/contact' % self.base()) + self.getPage("/defnoindex/page") + self.assertStatus((302, 303)) + self.assertHeader('Location', '%s/defnoindex/contact' % self.base()) + + self.getPage("/redirect") + self.assertStatus('302 Found') + self.assertHeader('Location', '%s/dir1/' % self.base()) + + if not getattr(cherrypy.server, "using_apache", False): + # Test that we can use URL's which aren't all valid Python identifiers + # This should also test the %XX-unquoting of URL's. + self.getPage("/Von%20B%fclow?ID=14") + self.assertBody("ID is 14") + + # Test that %2F in the path doesn't get unquoted too early; + # that is, it should not be used to separate path components. + # See ticket #393. + self.getPage("/page%2Fname") + self.assertBody("default:('page/name',)") + + self.getPage("/dir1/dir2/script_name") + self.assertBody(url) + self.getPage("/dir1/dir2/cherrypy_url") + self.assertBody("%s/extra" % self.base()) + + # Test that configs don't overwrite each other from diferent apps + self.getPage("/confvalue") + self.assertBody((url or "/").split("/")[-2]) + + self.script_name = "" + + # Test absoluteURI's in the Request-Line + self.getPage('http://%s:%s/' % (self.interface(), self.PORT)) + self.assertBody('world') + + self.getPage('http://%s:%s/abs/?service=http://192.168.0.1/x/y/z' % + (self.interface(), self.PORT)) + self.assertBody("default:('abs',)") + + self.getPage('/rel/?service=http://192.168.120.121:8000/x/y/z') + self.assertBody("default:('rel',)") + + # Test that the "isolated" app doesn't leak url's into the root app. + # If it did leak, Root.default() would answer with + # "default:('isolated', 'doesnt', 'exist')". + self.getPage("/isolated/") + self.assertStatus("200 OK") + self.assertBody("made it!") + self.getPage("/isolated/doesnt/exist") + self.assertStatus("404 Not Found") + + # Make sure /foobar maps to Root.foobar and not to the app + # mounted at /foo. See http://www.cherrypy.org/ticket/573 + self.getPage("/foobar") + self.assertBody("bar") + + def test_translate(self): + self.getPage("/translate_html") + self.assertStatus("200 OK") + self.assertBody("OK") + + self.getPage("/translate.html") + self.assertStatus("200 OK") + self.assertBody("OK") + + self.getPage("/translate-html") + self.assertStatus("200 OK") + self.assertBody("OK") + + def test_redir_using_url(self): + for url in script_names: + prefix = self.script_name = url + + # Test the absolute path to the parent (leading slash) + self.getPage('/redirect_via_url?path=./') + self.assertStatus(('302 Found', '303 See Other')) + self.assertHeader('Location', '%s/' % self.base()) + + # Test the relative path to the parent (no leading slash) + self.getPage('/redirect_via_url?path=./') + self.assertStatus(('302 Found', '303 See Other')) + self.assertHeader('Location', '%s/' % self.base()) + + # Test the absolute path to the parent (leading slash) + self.getPage('/redirect_via_url/?path=./') + self.assertStatus(('302 Found', '303 See Other')) + self.assertHeader('Location', '%s/' % self.base()) + + # Test the relative path to the parent (no leading slash) + self.getPage('/redirect_via_url/?path=./') + self.assertStatus(('302 Found', '303 See Other')) + self.assertHeader('Location', '%s/' % self.base()) + + def testPositionalParams(self): + self.getPage("/dir1/dir2/posparam/18/24/hut/hike") + self.assertBody("18/24/hut/hike") + + # intermediate index methods should not receive posparams; + # only the "final" index method should do so. + self.getPage("/dir1/dir2/5/3/sir") + self.assertBody("default for dir1, param is:('dir2', '5', '3', 'sir')") + + # test that extra positional args raises an 404 Not Found + # See http://www.cherrypy.org/ticket/733. + self.getPage("/dir1/dir2/script_name/extra/stuff") + self.assertStatus(404) + + def testExpose(self): + # Test the cherrypy.expose function/decorator + self.getPage("/exposing/base") + self.assertBody("expose works!") + + self.getPage("/exposing/1") + self.assertBody("expose works!") + + self.getPage("/exposing/2") + self.assertBody("expose works!") + + self.getPage("/exposingnew/base") + self.assertBody("expose works!") + + self.getPage("/exposingnew/1") + self.assertBody("expose works!") + + self.getPage("/exposingnew/2") + self.assertBody("expose works!") + + def testMethodDispatch(self): + self.getPage("/bymethod") + self.assertBody("['another']") + self.assertHeader('Allow', 'GET, HEAD, POST') + + self.getPage("/bymethod", method="HEAD") + self.assertBody("") + self.assertHeader('Allow', 'GET, HEAD, POST') + + self.getPage("/bymethod", method="POST", body="thing=one") + self.assertBody("") + self.assertHeader('Allow', 'GET, HEAD, POST') + + self.getPage("/bymethod") + self.assertBody(repr(['another', ntou('one')])) + self.assertHeader('Allow', 'GET, HEAD, POST') + + self.getPage("/bymethod", method="PUT") + self.assertErrorPage(405) + self.assertHeader('Allow', 'GET, HEAD, POST') + + # Test default with posparams + self.getPage("/collection/silly", method="POST") + self.getPage("/collection", method="GET") + self.assertBody("['a', 'bit', 'silly']") + + # Test custom dispatcher set on app root (see #737). + self.getPage("/app") + self.assertBody("milk") + + def testTreeMounting(self): + class Root(object): + def hello(self): + return "Hello world!" + hello.exposed = True + + # When mounting an application instance, + # we can't specify a different script name in the call to mount. + a = Application(Root(), '/somewhere') + self.assertRaises(ValueError, cherrypy.tree.mount, a, '/somewhereelse') + + # When mounting an application instance... + a = Application(Root(), '/somewhere') + # ...we MUST allow in identical script name in the call to mount... + cherrypy.tree.mount(a, '/somewhere') + self.getPage('/somewhere/hello') + self.assertStatus(200) + # ...and MUST allow a missing script_name. + del cherrypy.tree.apps['/somewhere'] + cherrypy.tree.mount(a) + self.getPage('/somewhere/hello') + self.assertStatus(200) + + # In addition, we MUST be able to create an Application using + # script_name == None for access to the wsgi_environ. + a = Application(Root(), script_name=None) + # However, this does not apply to tree.mount + self.assertRaises(TypeError, cherrypy.tree.mount, a, None) + diff --git a/libs/cherrypy/test/test_proxy.py b/libs/cherrypy/test/test_proxy.py new file mode 100644 index 0000000..2fbb619 --- /dev/null +++ b/libs/cherrypy/test/test_proxy.py @@ -0,0 +1,129 @@ +import cherrypy +from cherrypy.test import helper + +script_names = ["", "/path/to/myapp"] + + +class ProxyTest(helper.CPWebCase): + + def setup_server(): + + # Set up site + cherrypy.config.update({ + 'tools.proxy.on': True, + 'tools.proxy.base': 'www.mydomain.test', + }) + + # Set up application + + class Root: + + def __init__(self, sn): + # Calculate a URL outside of any requests. + self.thisnewpage = cherrypy.url("/this/new/page", script_name=sn) + + def pageurl(self): + return self.thisnewpage + pageurl.exposed = True + + def index(self): + raise cherrypy.HTTPRedirect('dummy') + index.exposed = True + + def remoteip(self): + return cherrypy.request.remote.ip + remoteip.exposed = True + + def xhost(self): + raise cherrypy.HTTPRedirect('blah') + xhost.exposed = True + xhost._cp_config = {'tools.proxy.local': 'X-Host', + 'tools.trailing_slash.extra': True, + } + + def base(self): + return cherrypy.request.base + base.exposed = True + + def ssl(self): + return cherrypy.request.base + ssl.exposed = True + ssl._cp_config = {'tools.proxy.scheme': 'X-Forwarded-Ssl'} + + def newurl(self): + return ("Browse to this page." + % cherrypy.url("/this/new/page")) + newurl.exposed = True + + for sn in script_names: + cherrypy.tree.mount(Root(sn), sn) + setup_server = staticmethod(setup_server) + + def testProxy(self): + self.getPage("/") + self.assertHeader('Location', + "%s://www.mydomain.test%s/dummy" % + (self.scheme, self.prefix())) + + # Test X-Forwarded-Host (Apache 1.3.33+ and Apache 2) + self.getPage("/", headers=[('X-Forwarded-Host', 'http://www.example.test')]) + self.assertHeader('Location', "http://www.example.test/dummy") + self.getPage("/", headers=[('X-Forwarded-Host', 'www.example.test')]) + self.assertHeader('Location', "%s://www.example.test/dummy" % self.scheme) + # Test multiple X-Forwarded-Host headers + self.getPage("/", headers=[ + ('X-Forwarded-Host', 'http://www.example.test, www.cherrypy.test'), + ]) + self.assertHeader('Location', "http://www.example.test/dummy") + + # Test X-Forwarded-For (Apache2) + self.getPage("/remoteip", + headers=[('X-Forwarded-For', '192.168.0.20')]) + self.assertBody("192.168.0.20") + self.getPage("/remoteip", + headers=[('X-Forwarded-For', '67.15.36.43, 192.168.0.20')]) + self.assertBody("192.168.0.20") + + # Test X-Host (lighttpd; see https://trac.lighttpd.net/trac/ticket/418) + self.getPage("/xhost", headers=[('X-Host', 'www.example.test')]) + self.assertHeader('Location', "%s://www.example.test/blah" % self.scheme) + + # Test X-Forwarded-Proto (lighttpd) + self.getPage("/base", headers=[('X-Forwarded-Proto', 'https')]) + self.assertBody("https://www.mydomain.test") + + # Test X-Forwarded-Ssl (webfaction?) + self.getPage("/ssl", headers=[('X-Forwarded-Ssl', 'on')]) + self.assertBody("https://www.mydomain.test") + + # Test cherrypy.url() + for sn in script_names: + # Test the value inside requests + self.getPage(sn + "/newurl") + self.assertBody("Browse to this page.") + self.getPage(sn + "/newurl", headers=[('X-Forwarded-Host', + 'http://www.example.test')]) + self.assertBody("Browse to this page.") + + # Test the value outside requests + port = "" + if self.scheme == "http" and self.PORT != 80: + port = ":%s" % self.PORT + elif self.scheme == "https" and self.PORT != 443: + port = ":%s" % self.PORT + host = self.HOST + if host in ('0.0.0.0', '::'): + import socket + host = socket.gethostname() + expected = ("%s://%s%s%s/this/new/page" + % (self.scheme, host, port, sn)) + self.getPage(sn + "/pageurl") + self.assertBody(expected) + + # Test trailing slash (see http://www.cherrypy.org/ticket/562). + self.getPage("/xhost/", headers=[('X-Host', 'www.example.test')]) + self.assertHeader('Location', "%s://www.example.test/xhost" + % self.scheme) + diff --git a/libs/cherrypy/test/test_refleaks.py b/libs/cherrypy/test/test_refleaks.py new file mode 100644 index 0000000..279935e --- /dev/null +++ b/libs/cherrypy/test/test_refleaks.py @@ -0,0 +1,59 @@ +"""Tests for refleaks.""" + +from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob +import threading + +import cherrypy + + +data = object() + + +from cherrypy.test import helper + + +class ReferenceTests(helper.CPWebCase): + + def setup_server(): + + class Root: + def index(self, *args, **kwargs): + cherrypy.request.thing = data + return "Hello world!" + index.exposed = True + + cherrypy.tree.mount(Root()) + setup_server = staticmethod(setup_server) + + def test_threadlocal_garbage(self): + success = [] + + def getpage(): + host = '%s:%s' % (self.interface(), self.PORT) + if self.scheme == 'https': + c = HTTPSConnection(host) + else: + c = HTTPConnection(host) + try: + c.putrequest('GET', '/') + c.endheaders() + response = c.getresponse() + body = response.read() + self.assertEqual(response.status, 200) + self.assertEqual(body, ntob("Hello world!")) + finally: + c.close() + success.append(True) + + ITERATIONS = 25 + ts = [] + for _ in range(ITERATIONS): + t = threading.Thread(target=getpage) + ts.append(t) + t.start() + + for t in ts: + t.join() + + self.assertEqual(len(success), ITERATIONS) + diff --git a/libs/cherrypy/test/test_request_obj.py b/libs/cherrypy/test/test_request_obj.py new file mode 100644 index 0000000..26eea56 --- /dev/null +++ b/libs/cherrypy/test/test_request_obj.py @@ -0,0 +1,737 @@ +"""Basic tests for the cherrypy.Request object.""" + +import os +localDir = os.path.dirname(__file__) +import sys +import types +from cherrypy._cpcompat import IncompleteRead, ntob, ntou, unicodestr + +import cherrypy +from cherrypy import _cptools, tools +from cherrypy.lib import httputil + +defined_http_methods = ("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", + "TRACE", "PROPFIND") + + +# Client-side code # + +from cherrypy.test import helper + +class RequestObjectTests(helper.CPWebCase): + + def setup_server(): + class Root: + + def index(self): + return "hello" + index.exposed = True + + def scheme(self): + return cherrypy.request.scheme + scheme.exposed = True + + root = Root() + + + class TestType(type): + """Metaclass which automatically exposes all functions in each subclass, + and adds an instance of the subclass as an attribute of root. + """ + def __init__(cls, name, bases, dct): + type.__init__(cls, name, bases, dct) + for value in dct.values(): + if isinstance(value, types.FunctionType): + value.exposed = True + setattr(root, name.lower(), cls()) + Test = TestType('Test', (object,), {}) + + class PathInfo(Test): + + def default(self, *args): + return cherrypy.request.path_info + + class Params(Test): + + def index(self, thing): + return repr(thing) + + def ismap(self, x, y): + return "Coordinates: %s, %s" % (x, y) + + def default(self, *args, **kwargs): + return "args: %s kwargs: %s" % (args, kwargs) + default._cp_config = {'request.query_string_encoding': 'latin1'} + + + class ParamErrorsCallable(object): + exposed = True + def __call__(self): + return "data" + + class ParamErrors(Test): + + def one_positional(self, param1): + return "data" + one_positional.exposed = True + + def one_positional_args(self, param1, *args): + return "data" + one_positional_args.exposed = True + + def one_positional_args_kwargs(self, param1, *args, **kwargs): + return "data" + one_positional_args_kwargs.exposed = True + + def one_positional_kwargs(self, param1, **kwargs): + return "data" + one_positional_kwargs.exposed = True + + def no_positional(self): + return "data" + no_positional.exposed = True + + def no_positional_args(self, *args): + return "data" + no_positional_args.exposed = True + + def no_positional_args_kwargs(self, *args, **kwargs): + return "data" + no_positional_args_kwargs.exposed = True + + def no_positional_kwargs(self, **kwargs): + return "data" + no_positional_kwargs.exposed = True + + callable_object = ParamErrorsCallable() + + def raise_type_error(self, **kwargs): + raise TypeError("Client Error") + raise_type_error.exposed = True + + def raise_type_error_with_default_param(self, x, y=None): + return '%d' % 'a' # throw an exception + raise_type_error_with_default_param.exposed = True + + def callable_error_page(status, **kwargs): + return "Error %s - Well, I'm very sorry but you haven't paid!" % status + + + class Error(Test): + + _cp_config = {'tools.log_tracebacks.on': True, + } + + def reason_phrase(self): + raise cherrypy.HTTPError("410 Gone fishin'") + + def custom(self, err='404'): + raise cherrypy.HTTPError(int(err), "No, really, not found!") + custom._cp_config = {'error_page.404': os.path.join(localDir, "static/index.html"), + 'error_page.401': callable_error_page, + } + + def custom_default(self): + return 1 + 'a' # raise an unexpected error + custom_default._cp_config = {'error_page.default': callable_error_page} + + def noexist(self): + raise cherrypy.HTTPError(404, "No, really, not found!") + noexist._cp_config = {'error_page.404': "nonexistent.html"} + + def page_method(self): + raise ValueError() + + def page_yield(self): + yield "howdy" + raise ValueError() + + def page_streamed(self): + yield "word up" + raise ValueError() + yield "very oops" + page_streamed._cp_config = {"response.stream": True} + + def cause_err_in_finalize(self): + # Since status must start with an int, this should error. + cherrypy.response.status = "ZOO OK" + cause_err_in_finalize._cp_config = {'request.show_tracebacks': False} + + def rethrow(self): + """Test that an error raised here will be thrown out to the server.""" + raise ValueError() + rethrow._cp_config = {'request.throw_errors': True} + + + class Expect(Test): + + def expectation_failed(self): + expect = cherrypy.request.headers.elements("Expect") + if expect and expect[0].value != '100-continue': + raise cherrypy.HTTPError(400) + raise cherrypy.HTTPError(417, 'Expectation Failed') + + class Headers(Test): + + def default(self, headername): + """Spit back out the value for the requested header.""" + return cherrypy.request.headers[headername] + + def doubledheaders(self): + # From http://www.cherrypy.org/ticket/165: + # "header field names should not be case sensitive sayes the rfc. + # if i set a headerfield in complete lowercase i end up with two + # header fields, one in lowercase, the other in mixed-case." + + # Set the most common headers + hMap = cherrypy.response.headers + hMap['content-type'] = "text/html" + hMap['content-length'] = 18 + hMap['server'] = 'CherryPy headertest' + hMap['location'] = ('%s://%s:%s/headers/' + % (cherrypy.request.local.ip, + cherrypy.request.local.port, + cherrypy.request.scheme)) + + # Set a rare header for fun + hMap['Expires'] = 'Thu, 01 Dec 2194 16:00:00 GMT' + + return "double header test" + + def ifmatch(self): + val = cherrypy.request.headers['If-Match'] + assert isinstance(val, unicodestr) + cherrypy.response.headers['ETag'] = val + return val + + + class HeaderElements(Test): + + def get_elements(self, headername): + e = cherrypy.request.headers.elements(headername) + return "\n".join([unicodestr(x) for x in e]) + + + class Method(Test): + + def index(self): + m = cherrypy.request.method + if m in defined_http_methods or m == "CONNECT": + return m + + if m == "LINK": + raise cherrypy.HTTPError(405) + else: + raise cherrypy.HTTPError(501) + + def parameterized(self, data): + return data + + def request_body(self): + # This should be a file object (temp file), + # which CP will just pipe back out if we tell it to. + return cherrypy.request.body + + def reachable(self): + return "success" + + class Divorce: + """HTTP Method handlers shouldn't collide with normal method names. + For example, a GET-handler shouldn't collide with a method named 'get'. + + If you build HTTP method dispatching into CherryPy, rewrite this class + to use your new dispatch mechanism and make sure that: + "GET /divorce HTTP/1.1" maps to divorce.index() and + "GET /divorce/get?ID=13 HTTP/1.1" maps to divorce.get() + """ + + documents = {} + + def index(self): + yield "

Choose your document

\n" + yield "
    \n" + for id, contents in self.documents.items(): + yield ("
  • %s: %s
  • \n" + % (id, id, contents)) + yield "
" + index.exposed = True + + def get(self, ID): + return ("Divorce document %s: %s" % + (ID, self.documents.get(ID, "empty"))) + get.exposed = True + + root.divorce = Divorce() + + + class ThreadLocal(Test): + + def index(self): + existing = repr(getattr(cherrypy.request, "asdf", None)) + cherrypy.request.asdf = "rassfrassin" + return existing + + appconf = { + '/method': {'request.methods_with_bodies': ("POST", "PUT", "PROPFIND")}, + } + cherrypy.tree.mount(root, config=appconf) + setup_server = staticmethod(setup_server) + + def test_scheme(self): + self.getPage("/scheme") + self.assertBody(self.scheme) + + def testRelativeURIPathInfo(self): + self.getPage("/pathinfo/foo/bar") + self.assertBody("/pathinfo/foo/bar") + + def testAbsoluteURIPathInfo(self): + # http://cherrypy.org/ticket/1061 + self.getPage("http://localhost/pathinfo/foo/bar") + self.assertBody("/pathinfo/foo/bar") + + def testParams(self): + self.getPage("/params/?thing=a") + self.assertBody(repr(ntou("a"))) + + self.getPage("/params/?thing=a&thing=b&thing=c") + self.assertBody(repr([ntou('a'), ntou('b'), ntou('c')])) + + # Test friendly error message when given params are not accepted. + cherrypy.config.update({"request.show_mismatched_params": True}) + self.getPage("/params/?notathing=meeting") + self.assertInBody("Missing parameters: thing") + self.getPage("/params/?thing=meeting¬athing=meeting") + self.assertInBody("Unexpected query string parameters: notathing") + + # Test ability to turn off friendly error messages + cherrypy.config.update({"request.show_mismatched_params": False}) + self.getPage("/params/?notathing=meeting") + self.assertInBody("Not Found") + self.getPage("/params/?thing=meeting¬athing=meeting") + self.assertInBody("Not Found") + + # Test "% HEX HEX"-encoded URL, param keys, and values + self.getPage("/params/%d4%20%e3/cheese?Gruy%E8re=Bulgn%e9ville") + self.assertBody("args: %s kwargs: %s" % + (('\xd4 \xe3', 'cheese'), + {'Gruy\xe8re': ntou('Bulgn\xe9ville')})) + + # Make sure that encoded = and & get parsed correctly + self.getPage("/params/code?url=http%3A//cherrypy.org/index%3Fa%3D1%26b%3D2") + self.assertBody("args: %s kwargs: %s" % + (('code',), + {'url': ntou('http://cherrypy.org/index?a=1&b=2')})) + + # Test coordinates sent by + self.getPage("/params/ismap?223,114") + self.assertBody("Coordinates: 223, 114") + + # Test "name[key]" dict-like params + self.getPage("/params/dictlike?a[1]=1&a[2]=2&b=foo&b[bar]=baz") + self.assertBody("args: %s kwargs: %s" % + (('dictlike',), + {'a[1]': ntou('1'), 'b[bar]': ntou('baz'), + 'b': ntou('foo'), 'a[2]': ntou('2')})) + + def testParamErrors(self): + + # test that all of the handlers work when given + # the correct parameters in order to ensure that the + # errors below aren't coming from some other source. + for uri in ( + '/paramerrors/one_positional?param1=foo', + '/paramerrors/one_positional_args?param1=foo', + '/paramerrors/one_positional_args/foo', + '/paramerrors/one_positional_args/foo/bar/baz', + '/paramerrors/one_positional_args_kwargs?param1=foo¶m2=bar', + '/paramerrors/one_positional_args_kwargs/foo?param2=bar¶m3=baz', + '/paramerrors/one_positional_args_kwargs/foo/bar/baz?param2=bar¶m3=baz', + '/paramerrors/one_positional_kwargs?param1=foo¶m2=bar¶m3=baz', + '/paramerrors/one_positional_kwargs/foo?param4=foo¶m2=bar¶m3=baz', + '/paramerrors/no_positional', + '/paramerrors/no_positional_args/foo', + '/paramerrors/no_positional_args/foo/bar/baz', + '/paramerrors/no_positional_args_kwargs?param1=foo¶m2=bar', + '/paramerrors/no_positional_args_kwargs/foo?param2=bar', + '/paramerrors/no_positional_args_kwargs/foo/bar/baz?param2=bar¶m3=baz', + '/paramerrors/no_positional_kwargs?param1=foo¶m2=bar', + '/paramerrors/callable_object', + ): + self.getPage(uri) + self.assertStatus(200) + + # query string parameters are part of the URI, so if they are wrong + # for a particular handler, the status MUST be a 404. + error_msgs = [ + 'Missing parameters', + 'Nothing matches the given URI', + 'Multiple values for parameters', + 'Unexpected query string parameters', + 'Unexpected body parameters', + ] + for uri, msg in ( + ('/paramerrors/one_positional', error_msgs[0]), + ('/paramerrors/one_positional?foo=foo', error_msgs[0]), + ('/paramerrors/one_positional/foo/bar/baz', error_msgs[1]), + ('/paramerrors/one_positional/foo?param1=foo', error_msgs[2]), + ('/paramerrors/one_positional/foo?param1=foo¶m2=foo', error_msgs[2]), + ('/paramerrors/one_positional_args/foo?param1=foo¶m2=foo', error_msgs[2]), + ('/paramerrors/one_positional_args/foo/bar/baz?param2=foo', error_msgs[3]), + ('/paramerrors/one_positional_args_kwargs/foo/bar/baz?param1=bar¶m3=baz', error_msgs[2]), + ('/paramerrors/one_positional_kwargs/foo?param1=foo¶m2=bar¶m3=baz', error_msgs[2]), + ('/paramerrors/no_positional/boo', error_msgs[1]), + ('/paramerrors/no_positional?param1=foo', error_msgs[3]), + ('/paramerrors/no_positional_args/boo?param1=foo', error_msgs[3]), + ('/paramerrors/no_positional_kwargs/boo?param1=foo', error_msgs[1]), + ('/paramerrors/callable_object?param1=foo', error_msgs[3]), + ('/paramerrors/callable_object/boo', error_msgs[1]), + ): + for show_mismatched_params in (True, False): + cherrypy.config.update({'request.show_mismatched_params': show_mismatched_params}) + self.getPage(uri) + self.assertStatus(404) + if show_mismatched_params: + self.assertInBody(msg) + else: + self.assertInBody("Not Found") + + # if body parameters are wrong, a 400 must be returned. + for uri, body, msg in ( + ('/paramerrors/one_positional/foo', 'param1=foo', error_msgs[2]), + ('/paramerrors/one_positional/foo', 'param1=foo¶m2=foo', error_msgs[2]), + ('/paramerrors/one_positional_args/foo', 'param1=foo¶m2=foo', error_msgs[2]), + ('/paramerrors/one_positional_args/foo/bar/baz', 'param2=foo', error_msgs[4]), + ('/paramerrors/one_positional_args_kwargs/foo/bar/baz', 'param1=bar¶m3=baz', error_msgs[2]), + ('/paramerrors/one_positional_kwargs/foo', 'param1=foo¶m2=bar¶m3=baz', error_msgs[2]), + ('/paramerrors/no_positional', 'param1=foo', error_msgs[4]), + ('/paramerrors/no_positional_args/boo', 'param1=foo', error_msgs[4]), + ('/paramerrors/callable_object', 'param1=foo', error_msgs[4]), + ): + for show_mismatched_params in (True, False): + cherrypy.config.update({'request.show_mismatched_params': show_mismatched_params}) + self.getPage(uri, method='POST', body=body) + self.assertStatus(400) + if show_mismatched_params: + self.assertInBody(msg) + else: + self.assertInBody("400 Bad") + + + # even if body parameters are wrong, if we get the uri wrong, then + # it's a 404 + for uri, body, msg in ( + ('/paramerrors/one_positional?param2=foo', 'param1=foo', error_msgs[3]), + ('/paramerrors/one_positional/foo/bar', 'param2=foo', error_msgs[1]), + ('/paramerrors/one_positional_args/foo/bar?param2=foo', 'param3=foo', error_msgs[3]), + ('/paramerrors/one_positional_kwargs/foo/bar', 'param2=bar¶m3=baz', error_msgs[1]), + ('/paramerrors/no_positional?param1=foo', 'param2=foo', error_msgs[3]), + ('/paramerrors/no_positional_args/boo?param2=foo', 'param1=foo', error_msgs[3]), + ('/paramerrors/callable_object?param2=bar', 'param1=foo', error_msgs[3]), + ): + for show_mismatched_params in (True, False): + cherrypy.config.update({'request.show_mismatched_params': show_mismatched_params}) + self.getPage(uri, method='POST', body=body) + self.assertStatus(404) + if show_mismatched_params: + self.assertInBody(msg) + else: + self.assertInBody("Not Found") + + # In the case that a handler raises a TypeError we should + # let that type error through. + for uri in ( + '/paramerrors/raise_type_error', + '/paramerrors/raise_type_error_with_default_param?x=0', + '/paramerrors/raise_type_error_with_default_param?x=0&y=0', + ): + self.getPage(uri, method='GET') + self.assertStatus(500) + self.assertTrue('Client Error', self.body) + + def testErrorHandling(self): + self.getPage("/error/missing") + self.assertStatus(404) + self.assertErrorPage(404, "The path '/error/missing' was not found.") + + ignore = helper.webtest.ignored_exceptions + ignore.append(ValueError) + try: + valerr = '\n raise ValueError()\nValueError' + self.getPage("/error/page_method") + self.assertErrorPage(500, pattern=valerr) + + self.getPage("/error/page_yield") + self.assertErrorPage(500, pattern=valerr) + + if (cherrypy.server.protocol_version == "HTTP/1.0" or + getattr(cherrypy.server, "using_apache", False)): + self.getPage("/error/page_streamed") + # Because this error is raised after the response body has + # started, the status should not change to an error status. + self.assertStatus(200) + self.assertBody("word up") + else: + # Under HTTP/1.1, the chunked transfer-coding is used. + # The HTTP client will choke when the output is incomplete. + self.assertRaises((ValueError, IncompleteRead), self.getPage, + "/error/page_streamed") + + # No traceback should be present + self.getPage("/error/cause_err_in_finalize") + msg = "Illegal response status from server ('ZOO' is non-numeric)." + self.assertErrorPage(500, msg, None) + finally: + ignore.pop() + + # Test HTTPError with a reason-phrase in the status arg. + self.getPage('/error/reason_phrase') + self.assertStatus("410 Gone fishin'") + + # Test custom error page for a specific error. + self.getPage("/error/custom") + self.assertStatus(404) + self.assertBody("Hello, world\r\n" + (" " * 499)) + + # Test custom error page for a specific error. + self.getPage("/error/custom?err=401") + self.assertStatus(401) + self.assertBody("Error 401 Unauthorized - Well, I'm very sorry but you haven't paid!") + + # Test default custom error page. + self.getPage("/error/custom_default") + self.assertStatus(500) + self.assertBody("Error 500 Internal Server Error - Well, I'm very sorry but you haven't paid!".ljust(513)) + + # Test error in custom error page (ticket #305). + # Note that the message is escaped for HTML (ticket #310). + self.getPage("/error/noexist") + self.assertStatus(404) + msg = ("No, <b>really</b>, not found!
" + "In addition, the custom error page failed:\n
" + "IOError: [Errno 2] No such file or directory: 'nonexistent.html'") + self.assertInBody(msg) + + if getattr(cherrypy.server, "using_apache", False): + pass + else: + # Test throw_errors (ticket #186). + self.getPage("/error/rethrow") + self.assertInBody("raise ValueError()") + + def testExpect(self): + e = ('Expect', '100-continue') + self.getPage("/headerelements/get_elements?headername=Expect", [e]) + self.assertBody('100-continue') + + self.getPage("/expect/expectation_failed", [e]) + self.assertStatus(417) + + def testHeaderElements(self): + # Accept-* header elements should be sorted, with most preferred first. + h = [('Accept', 'audio/*; q=0.2, audio/basic')] + self.getPage("/headerelements/get_elements?headername=Accept", h) + self.assertStatus(200) + self.assertBody("audio/basic\n" + "audio/*;q=0.2") + + h = [('Accept', 'text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c')] + self.getPage("/headerelements/get_elements?headername=Accept", h) + self.assertStatus(200) + self.assertBody("text/x-c\n" + "text/html\n" + "text/x-dvi;q=0.8\n" + "text/plain;q=0.5") + + # Test that more specific media ranges get priority. + h = [('Accept', 'text/*, text/html, text/html;level=1, */*')] + self.getPage("/headerelements/get_elements?headername=Accept", h) + self.assertStatus(200) + self.assertBody("text/html;level=1\n" + "text/html\n" + "text/*\n" + "*/*") + + # Test Accept-Charset + h = [('Accept-Charset', 'iso-8859-5, unicode-1-1;q=0.8')] + self.getPage("/headerelements/get_elements?headername=Accept-Charset", h) + self.assertStatus("200 OK") + self.assertBody("iso-8859-5\n" + "unicode-1-1;q=0.8") + + # Test Accept-Encoding + h = [('Accept-Encoding', 'gzip;q=1.0, identity; q=0.5, *;q=0')] + self.getPage("/headerelements/get_elements?headername=Accept-Encoding", h) + self.assertStatus("200 OK") + self.assertBody("gzip;q=1.0\n" + "identity;q=0.5\n" + "*;q=0") + + # Test Accept-Language + h = [('Accept-Language', 'da, en-gb;q=0.8, en;q=0.7')] + self.getPage("/headerelements/get_elements?headername=Accept-Language", h) + self.assertStatus("200 OK") + self.assertBody("da\n" + "en-gb;q=0.8\n" + "en;q=0.7") + + # Test malformed header parsing. See http://www.cherrypy.org/ticket/763. + self.getPage("/headerelements/get_elements?headername=Content-Type", + # Note the illegal trailing ";" + headers=[('Content-Type', 'text/html; charset=utf-8;')]) + self.assertStatus(200) + self.assertBody("text/html;charset=utf-8") + + def test_repeated_headers(self): + # Test that two request headers are collapsed into one. + # See http://www.cherrypy.org/ticket/542. + self.getPage("/headers/Accept-Charset", + headers=[("Accept-Charset", "iso-8859-5"), + ("Accept-Charset", "unicode-1-1;q=0.8")]) + self.assertBody("iso-8859-5, unicode-1-1;q=0.8") + + # Tests that each header only appears once, regardless of case. + self.getPage("/headers/doubledheaders") + self.assertBody("double header test") + hnames = [name.title() for name, val in self.headers] + for key in ['Content-Length', 'Content-Type', 'Date', + 'Expires', 'Location', 'Server']: + self.assertEqual(hnames.count(key), 1, self.headers) + + def test_encoded_headers(self): + # First, make sure the innards work like expected. + self.assertEqual(httputil.decode_TEXT(ntou("=?utf-8?q?f=C3=BCr?=")), ntou("f\xfcr")) + + if cherrypy.server.protocol_version == "HTTP/1.1": + # Test RFC-2047-encoded request and response header values + u = ntou('\u212bngstr\xf6m', 'escape') + c = ntou("=E2=84=ABngstr=C3=B6m") + self.getPage("/headers/ifmatch", [('If-Match', ntou('=?utf-8?q?%s?=') % c)]) + # The body should be utf-8 encoded. + self.assertBody(ntob("\xe2\x84\xabngstr\xc3\xb6m")) + # But the Etag header should be RFC-2047 encoded (binary) + self.assertHeader("ETag", ntou('=?utf-8?b?4oSrbmdzdHLDtm0=?=')) + + # Test a *LONG* RFC-2047-encoded request and response header value + self.getPage("/headers/ifmatch", + [('If-Match', ntou('=?utf-8?q?%s?=') % (c * 10))]) + self.assertBody(ntob("\xe2\x84\xabngstr\xc3\xb6m") * 10) + # Note: this is different output for Python3, but it decodes fine. + etag = self.assertHeader("ETag", + '=?utf-8?b?4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' + '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' + '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' + '4oSrbmdzdHLDtm0=?=') + self.assertEqual(httputil.decode_TEXT(etag), u * 10) + + def test_header_presence(self): + # If we don't pass a Content-Type header, it should not be present + # in cherrypy.request.headers + self.getPage("/headers/Content-Type", + headers=[]) + self.assertStatus(500) + + # If Content-Type is present in the request, it should be present in + # cherrypy.request.headers + self.getPage("/headers/Content-Type", + headers=[("Content-type", "application/json")]) + self.assertBody("application/json") + + def test_basic_HTTPMethods(self): + helper.webtest.methods_with_bodies = ("POST", "PUT", "PROPFIND") + + # Test that all defined HTTP methods work. + for m in defined_http_methods: + self.getPage("/method/", method=m) + + # HEAD requests should not return any body. + if m == "HEAD": + self.assertBody("") + elif m == "TRACE": + # Some HTTP servers (like modpy) have their own TRACE support + self.assertEqual(self.body[:5], ntob("TRACE")) + else: + self.assertBody(m) + + # Request a PUT method with a form-urlencoded body + self.getPage("/method/parameterized", method="PUT", + body="data=on+top+of+other+things") + self.assertBody("on top of other things") + + # Request a PUT method with a file body + b = "one thing on top of another" + h = [("Content-Type", "text/plain"), + ("Content-Length", str(len(b)))] + self.getPage("/method/request_body", headers=h, method="PUT", body=b) + self.assertStatus(200) + self.assertBody(b) + + # Request a PUT method with a file body but no Content-Type. + # See http://www.cherrypy.org/ticket/790. + b = ntob("one thing on top of another") + self.persistent = True + try: + conn = self.HTTP_CONN + conn.putrequest("PUT", "/method/request_body", skip_host=True) + conn.putheader("Host", self.HOST) + conn.putheader('Content-Length', str(len(b))) + conn.endheaders() + conn.send(b) + response = conn.response_class(conn.sock, method="PUT") + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(b) + finally: + self.persistent = False + + # Request a PUT method with no body whatsoever (not an empty one). + # See http://www.cherrypy.org/ticket/650. + # Provide a C-T or webtest will provide one (and a C-L) for us. + h = [("Content-Type", "text/plain")] + self.getPage("/method/reachable", headers=h, method="PUT") + self.assertStatus(411) + + # Request a custom method with a request body + b = ('\n\n' + '' + '') + h = [('Content-Type', 'text/xml'), + ('Content-Length', str(len(b)))] + self.getPage("/method/request_body", headers=h, method="PROPFIND", body=b) + self.assertStatus(200) + self.assertBody(b) + + # Request a disallowed method + self.getPage("/method/", method="LINK") + self.assertStatus(405) + + # Request an unknown method + self.getPage("/method/", method="SEARCH") + self.assertStatus(501) + + # For method dispatchers: make sure that an HTTP method doesn't + # collide with a virtual path atom. If you build HTTP-method + # dispatching into the core, rewrite these handlers to use + # your dispatch idioms. + self.getPage("/divorce/get?ID=13") + self.assertBody('Divorce document 13: empty') + self.assertStatus(200) + self.getPage("/divorce/", method="GET") + self.assertBody('

Choose your document

\n
    \n
') + self.assertStatus(200) + + def test_CONNECT_method(self): + if getattr(cherrypy.server, "using_apache", False): + return self.skip("skipped due to known Apache differences... ") + + self.getPage("/method/", method="CONNECT") + self.assertBody("CONNECT") + + def testEmptyThreadlocals(self): + results = [] + for x in range(20): + self.getPage("/threadlocal/") + results.append(self.body) + self.assertEqual(results, [ntob("None")] * 20) + diff --git a/libs/cherrypy/test/test_routes.py b/libs/cherrypy/test/test_routes.py new file mode 100644 index 0000000..a8062f8 --- /dev/null +++ b/libs/cherrypy/test/test_routes.py @@ -0,0 +1,69 @@ +import os +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + +import cherrypy + +from cherrypy.test import helper +import nose + +class RoutesDispatchTest(helper.CPWebCase): + + def setup_server(): + + try: + import routes + except ImportError: + raise nose.SkipTest('Install routes to test RoutesDispatcher code') + + class Dummy: + def index(self): + return "I said good day!" + + class City: + + def __init__(self, name): + self.name = name + self.population = 10000 + + def index(self, **kwargs): + return "Welcome to %s, pop. %s" % (self.name, self.population) + index._cp_config = {'tools.response_headers.on': True, + 'tools.response_headers.headers': [('Content-Language', 'en-GB')]} + + def update(self, **kwargs): + self.population = kwargs['pop'] + return "OK" + + d = cherrypy.dispatch.RoutesDispatcher() + d.connect(action='index', name='hounslow', route='/hounslow', + controller=City('Hounslow')) + d.connect(name='surbiton', route='/surbiton', controller=City('Surbiton'), + action='index', conditions=dict(method=['GET'])) + d.mapper.connect('/surbiton', controller='surbiton', + action='update', conditions=dict(method=['POST'])) + d.connect('main', ':action', controller=Dummy()) + + conf = {'/': {'request.dispatch': d}} + cherrypy.tree.mount(root=None, config=conf) + setup_server = staticmethod(setup_server) + + def test_Routes_Dispatch(self): + self.getPage("/hounslow") + self.assertStatus("200 OK") + self.assertBody("Welcome to Hounslow, pop. 10000") + + self.getPage("/foo") + self.assertStatus("404 Not Found") + + self.getPage("/surbiton") + self.assertStatus("200 OK") + self.assertBody("Welcome to Surbiton, pop. 10000") + + self.getPage("/surbiton", method="POST", body="pop=1327") + self.assertStatus("200 OK") + self.assertBody("OK") + self.getPage("/surbiton") + self.assertStatus("200 OK") + self.assertHeader("Content-Language", "en-GB") + self.assertBody("Welcome to Surbiton, pop. 1327") + diff --git a/libs/cherrypy/test/test_session.py b/libs/cherrypy/test/test_session.py new file mode 100644 index 0000000..9143a1d --- /dev/null +++ b/libs/cherrypy/test/test_session.py @@ -0,0 +1,464 @@ +import os +localDir = os.path.dirname(__file__) +import sys +import threading +import time + +import cherrypy +from cherrypy._cpcompat import copykeys, HTTPConnection, HTTPSConnection +from cherrypy.lib import sessions +from cherrypy.lib.httputil import response_codes + +def http_methods_allowed(methods=['GET', 'HEAD']): + method = cherrypy.request.method.upper() + if method not in methods: + cherrypy.response.headers['Allow'] = ", ".join(methods) + raise cherrypy.HTTPError(405) + +cherrypy.tools.allow = cherrypy.Tool('on_start_resource', http_methods_allowed) + + +def setup_server(): + + class Root: + + _cp_config = {'tools.sessions.on': True, + 'tools.sessions.storage_type' : 'ram', + 'tools.sessions.storage_path' : localDir, + 'tools.sessions.timeout': (1.0 / 60), + 'tools.sessions.clean_freq': (1.0 / 60), + } + + def clear(self): + cherrypy.session.cache.clear() + clear.exposed = True + + def data(self): + cherrypy.session['aha'] = 'foo' + return repr(cherrypy.session._data) + data.exposed = True + + def testGen(self): + counter = cherrypy.session.get('counter', 0) + 1 + cherrypy.session['counter'] = counter + yield str(counter) + testGen.exposed = True + + def testStr(self): + counter = cherrypy.session.get('counter', 0) + 1 + cherrypy.session['counter'] = counter + return str(counter) + testStr.exposed = True + + def setsessiontype(self, newtype): + self.__class__._cp_config.update({'tools.sessions.storage_type': newtype}) + if hasattr(cherrypy, "session"): + del cherrypy.session + cls = getattr(sessions, newtype.title() + 'Session') + if cls.clean_thread: + cls.clean_thread.stop() + cls.clean_thread.unsubscribe() + del cls.clean_thread + setsessiontype.exposed = True + setsessiontype._cp_config = {'tools.sessions.on': False} + + def index(self): + sess = cherrypy.session + c = sess.get('counter', 0) + 1 + time.sleep(0.01) + sess['counter'] = c + return str(c) + index.exposed = True + + def keyin(self, key): + return str(key in cherrypy.session) + keyin.exposed = True + + def delete(self): + cherrypy.session.delete() + sessions.expire() + return "done" + delete.exposed = True + + def delkey(self, key): + del cherrypy.session[key] + return "OK" + delkey.exposed = True + + def blah(self): + return self._cp_config['tools.sessions.storage_type'] + blah.exposed = True + + def iredir(self): + raise cherrypy.InternalRedirect('/blah') + iredir.exposed = True + + def restricted(self): + return cherrypy.request.method + restricted.exposed = True + restricted._cp_config = {'tools.allow.on': True, + 'tools.allow.methods': ['GET']} + + def regen(self): + cherrypy.tools.sessions.regenerate() + return "logged in" + regen.exposed = True + + def length(self): + return str(len(cherrypy.session)) + length.exposed = True + + def session_cookie(self): + # Must load() to start the clean thread. + cherrypy.session.load() + return cherrypy.session.id + session_cookie.exposed = True + session_cookie._cp_config = { + 'tools.sessions.path': '/session_cookie', + 'tools.sessions.name': 'temp', + 'tools.sessions.persistent': False} + + cherrypy.tree.mount(Root()) + + +from cherrypy.test import helper + +class SessionTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def tearDown(self): + # Clean up sessions. + for fname in os.listdir(localDir): + if fname.startswith(sessions.FileSession.SESSION_PREFIX): + os.unlink(os.path.join(localDir, fname)) + + def test_0_Session(self): + self.getPage('/setsessiontype/ram') + self.getPage('/clear') + + # Test that a normal request gets the same id in the cookies. + # Note: this wouldn't work if /data didn't load the session. + self.getPage('/data') + self.assertBody("{'aha': 'foo'}") + c = self.cookies[0] + self.getPage('/data', self.cookies) + self.assertEqual(self.cookies[0], c) + + self.getPage('/testStr') + self.assertBody('1') + cookie_parts = dict([p.strip().split('=') + for p in self.cookies[0][1].split(";")]) + # Assert there is an 'expires' param + self.assertEqual(set(cookie_parts.keys()), + set(['session_id', 'expires', 'Path'])) + self.getPage('/testGen', self.cookies) + self.assertBody('2') + self.getPage('/testStr', self.cookies) + self.assertBody('3') + self.getPage('/data', self.cookies) + self.assertBody("{'aha': 'foo', 'counter': 3}") + self.getPage('/length', self.cookies) + self.assertBody('2') + self.getPage('/delkey?key=counter', self.cookies) + self.assertStatus(200) + + self.getPage('/setsessiontype/file') + self.getPage('/testStr') + self.assertBody('1') + self.getPage('/testGen', self.cookies) + self.assertBody('2') + self.getPage('/testStr', self.cookies) + self.assertBody('3') + self.getPage('/delkey?key=counter', self.cookies) + self.assertStatus(200) + + # Wait for the session.timeout (1 second) + time.sleep(2) + self.getPage('/') + self.assertBody('1') + self.getPage('/length', self.cookies) + self.assertBody('1') + + # Test session __contains__ + self.getPage('/keyin?key=counter', self.cookies) + self.assertBody("True") + cookieset1 = self.cookies + + # Make a new session and test __len__ again + self.getPage('/') + self.getPage('/length', self.cookies) + self.assertBody('2') + + # Test session delete + self.getPage('/delete', self.cookies) + self.assertBody("done") + self.getPage('/delete', cookieset1) + self.assertBody("done") + f = lambda: [x for x in os.listdir(localDir) if x.startswith('session-')] + self.assertEqual(f(), []) + + # Wait for the cleanup thread to delete remaining session files + self.getPage('/') + f = lambda: [x for x in os.listdir(localDir) if x.startswith('session-')] + self.assertNotEqual(f(), []) + time.sleep(2) + self.assertEqual(f(), []) + + def test_1_Ram_Concurrency(self): + self.getPage('/setsessiontype/ram') + self._test_Concurrency() + + def test_2_File_Concurrency(self): + self.getPage('/setsessiontype/file') + self._test_Concurrency() + + def _test_Concurrency(self): + client_thread_count = 5 + request_count = 30 + + # Get initial cookie + self.getPage("/") + self.assertBody("1") + cookies = self.cookies + + data_dict = {} + errors = [] + + def request(index): + if self.scheme == 'https': + c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) + else: + c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) + for i in range(request_count): + c.putrequest('GET', '/') + for k, v in cookies: + c.putheader(k, v) + c.endheaders() + response = c.getresponse() + body = response.read() + if response.status != 200 or not body.isdigit(): + errors.append((response.status, body)) + else: + data_dict[index] = max(data_dict[index], int(body)) + # Uncomment the following line to prove threads overlap. +## sys.stdout.write("%d " % index) + + # Start requests from each of + # concurrent clients + ts = [] + for c in range(client_thread_count): + data_dict[c] = 0 + t = threading.Thread(target=request, args=(c,)) + ts.append(t) + t.start() + + for t in ts: + t.join() + + hitcount = max(data_dict.values()) + expected = 1 + (client_thread_count * request_count) + + for e in errors: + print(e) + self.assertEqual(hitcount, expected) + + def test_3_Redirect(self): + # Start a new session + self.getPage('/testStr') + self.getPage('/iredir', self.cookies) + self.assertBody("file") + + def test_4_File_deletion(self): + # Start a new session + self.getPage('/testStr') + # Delete the session file manually and retry. + id = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1] + path = os.path.join(localDir, "session-" + id) + os.unlink(path) + self.getPage('/testStr', self.cookies) + + def test_5_Error_paths(self): + self.getPage('/unknown/page') + self.assertErrorPage(404, "The path '/unknown/page' was not found.") + + # Note: this path is *not* the same as above. The above + # takes a normal route through the session code; this one + # skips the session code's before_handler and only calls + # before_finalize (save) and on_end (close). So the session + # code has to survive calling save/close without init. + self.getPage('/restricted', self.cookies, method='POST') + self.assertErrorPage(405, response_codes[405][1]) + + def test_6_regenerate(self): + self.getPage('/testStr') + # grab the cookie ID + id1 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1] + self.getPage('/regen') + self.assertBody('logged in') + id2 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1] + self.assertNotEqual(id1, id2) + + self.getPage('/testStr') + # grab the cookie ID + id1 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1] + self.getPage('/testStr', + headers=[('Cookie', + 'session_id=maliciousid; ' + 'expires=Sat, 27 Oct 2017 04:18:28 GMT; Path=/;')]) + id2 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1] + self.assertNotEqual(id1, id2) + self.assertNotEqual(id2, 'maliciousid') + + def test_7_session_cookies(self): + self.getPage('/setsessiontype/ram') + self.getPage('/clear') + self.getPage('/session_cookie') + # grab the cookie ID + cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(";")]) + # Assert there is no 'expires' param + self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path'])) + id1 = cookie_parts['temp'] + self.assertEqual(copykeys(sessions.RamSession.cache), [id1]) + + # Send another request in the same "browser session". + self.getPage('/session_cookie', self.cookies) + cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(";")]) + # Assert there is no 'expires' param + self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path'])) + self.assertBody(id1) + self.assertEqual(copykeys(sessions.RamSession.cache), [id1]) + + # Simulate a browser close by just not sending the cookies + self.getPage('/session_cookie') + # grab the cookie ID + cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(";")]) + # Assert there is no 'expires' param + self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path'])) + # Assert a new id has been generated... + id2 = cookie_parts['temp'] + self.assertNotEqual(id1, id2) + self.assertEqual(set(sessions.RamSession.cache.keys()), set([id1, id2])) + + # Wait for the session.timeout on both sessions + time.sleep(2.5) + cache = copykeys(sessions.RamSession.cache) + if cache: + if cache == [id2]: + self.fail("The second session did not time out.") + else: + self.fail("Unknown session id in cache: %r", cache) + + +import socket +try: + import memcache + + host, port = '127.0.0.1', 11211 + for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + s = None + try: + s = socket.socket(af, socktype, proto) + # See http://groups.google.com/group/cherrypy-users/ + # browse_frm/thread/bbfe5eb39c904fe0 + s.settimeout(1.0) + s.connect((host, port)) + s.close() + except socket.error: + if s: + s.close() + raise + break +except (ImportError, socket.error): + class MemcachedSessionTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test(self): + return self.skip("memcached not reachable ") +else: + class MemcachedSessionTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_0_Session(self): + self.getPage('/setsessiontype/memcached') + + self.getPage('/testStr') + self.assertBody('1') + self.getPage('/testGen', self.cookies) + self.assertBody('2') + self.getPage('/testStr', self.cookies) + self.assertBody('3') + self.getPage('/length', self.cookies) + self.assertErrorPage(500) + self.assertInBody("NotImplementedError") + self.getPage('/delkey?key=counter', self.cookies) + self.assertStatus(200) + + # Wait for the session.timeout (1 second) + time.sleep(1.25) + self.getPage('/') + self.assertBody('1') + + # Test session __contains__ + self.getPage('/keyin?key=counter', self.cookies) + self.assertBody("True") + + # Test session delete + self.getPage('/delete', self.cookies) + self.assertBody("done") + + def test_1_Concurrency(self): + client_thread_count = 5 + request_count = 30 + + # Get initial cookie + self.getPage("/") + self.assertBody("1") + cookies = self.cookies + + data_dict = {} + + def request(index): + for i in range(request_count): + self.getPage("/", cookies) + # Uncomment the following line to prove threads overlap. +## sys.stdout.write("%d " % index) + if not self.body.isdigit(): + self.fail(self.body) + data_dict[index] = v = int(self.body) + + # Start concurrent requests from + # each of clients + ts = [] + for c in range(client_thread_count): + data_dict[c] = 0 + t = threading.Thread(target=request, args=(c,)) + ts.append(t) + t.start() + + for t in ts: + t.join() + + hitcount = max(data_dict.values()) + expected = 1 + (client_thread_count * request_count) + self.assertEqual(hitcount, expected) + + def test_3_Redirect(self): + # Start a new session + self.getPage('/testStr') + self.getPage('/iredir', self.cookies) + self.assertBody("memcached") + + def test_5_Error_paths(self): + self.getPage('/unknown/page') + self.assertErrorPage(404, "The path '/unknown/page' was not found.") + + # Note: this path is *not* the same as above. The above + # takes a normal route through the session code; this one + # skips the session code's before_handler and only calls + # before_finalize (save) and on_end (close). So the session + # code has to survive calling save/close without init. + self.getPage('/restricted', self.cookies, method='POST') + self.assertErrorPage(405, response_codes[405][1]) + diff --git a/libs/cherrypy/test/test_sessionauthenticate.py b/libs/cherrypy/test/test_sessionauthenticate.py new file mode 100644 index 0000000..ab1fe51 --- /dev/null +++ b/libs/cherrypy/test/test_sessionauthenticate.py @@ -0,0 +1,62 @@ +import cherrypy +from cherrypy.test import helper + + +class SessionAuthenticateTest(helper.CPWebCase): + + def setup_server(): + + def check(username, password): + # Dummy check_username_and_password function + if username != 'test' or password != 'password': + return 'Wrong login/password' + + def augment_params(): + # A simple tool to add some things to request.params + # This is to check to make sure that session_auth can handle request + # params (ticket #780) + cherrypy.request.params["test"] = "test" + + cherrypy.tools.augment_params = cherrypy.Tool('before_handler', + augment_params, None, priority=30) + + class Test: + + _cp_config = {'tools.sessions.on': True, + 'tools.session_auth.on': True, + 'tools.session_auth.check_username_and_password': check, + 'tools.augment_params.on': True, + } + + def index(self, **kwargs): + return "Hi %s, you are logged in" % cherrypy.request.login + index.exposed = True + + cherrypy.tree.mount(Test()) + setup_server = staticmethod(setup_server) + + + def testSessionAuthenticate(self): + # request a page and check for login form + self.getPage('/') + self.assertInBody('
') + + # setup credentials + login_body = 'username=test&password=password&from_page=/' + + # attempt a login + self.getPage('/do_login', method='POST', body=login_body) + self.assertStatus((302, 303)) + + # get the page now that we are logged in + self.getPage('/', self.cookies) + self.assertBody('Hi test, you are logged in') + + # do a logout + self.getPage('/do_logout', self.cookies, method='POST') + self.assertStatus((302, 303)) + + # verify we are logged out + self.getPage('/', self.cookies) + self.assertInBody('') + diff --git a/libs/cherrypy/test/test_states.py b/libs/cherrypy/test/test_states.py new file mode 100644 index 0000000..6322687 --- /dev/null +++ b/libs/cherrypy/test/test_states.py @@ -0,0 +1,439 @@ +from cherrypy._cpcompat import BadStatusLine, ntob +import os +import sys +import threading +import time + +import cherrypy +engine = cherrypy.engine +thisdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +class Dependency: + + def __init__(self, bus): + self.bus = bus + self.running = False + self.startcount = 0 + self.gracecount = 0 + self.threads = {} + + def subscribe(self): + self.bus.subscribe('start', self.start) + self.bus.subscribe('stop', self.stop) + self.bus.subscribe('graceful', self.graceful) + self.bus.subscribe('start_thread', self.startthread) + self.bus.subscribe('stop_thread', self.stopthread) + + def start(self): + self.running = True + self.startcount += 1 + + def stop(self): + self.running = False + + def graceful(self): + self.gracecount += 1 + + def startthread(self, thread_id): + self.threads[thread_id] = None + + def stopthread(self, thread_id): + del self.threads[thread_id] + +db_connection = Dependency(engine) + +def setup_server(): + class Root: + def index(self): + return "Hello World" + index.exposed = True + + def ctrlc(self): + raise KeyboardInterrupt() + ctrlc.exposed = True + + def graceful(self): + engine.graceful() + return "app was (gracefully) restarted succesfully" + graceful.exposed = True + + def block_explicit(self): + while True: + if cherrypy.response.timed_out: + cherrypy.response.timed_out = False + return "broken!" + time.sleep(0.01) + block_explicit.exposed = True + + def block_implicit(self): + time.sleep(0.5) + return "response.timeout = %s" % cherrypy.response.timeout + block_implicit.exposed = True + + cherrypy.tree.mount(Root()) + cherrypy.config.update({ + 'environment': 'test_suite', + 'engine.deadlock_poll_freq': 0.1, + }) + + db_connection.subscribe() + + + +# ------------ Enough helpers. Time for real live test cases. ------------ # + + +from cherrypy.test import helper + +class ServerStateTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def setUp(self): + cherrypy.server.socket_timeout = 0.1 + self.do_gc_test = False + + def test_0_NormalStateFlow(self): + engine.stop() + # Our db_connection should not be running + self.assertEqual(db_connection.running, False) + self.assertEqual(db_connection.startcount, 1) + self.assertEqual(len(db_connection.threads), 0) + + # Test server start + engine.start() + self.assertEqual(engine.state, engine.states.STARTED) + + host = cherrypy.server.socket_host + port = cherrypy.server.socket_port + self.assertRaises(IOError, cherrypy._cpserver.check_port, host, port) + + # The db_connection should be running now + self.assertEqual(db_connection.running, True) + self.assertEqual(db_connection.startcount, 2) + self.assertEqual(len(db_connection.threads), 0) + + self.getPage("/") + self.assertBody("Hello World") + self.assertEqual(len(db_connection.threads), 1) + + # Test engine stop. This will also stop the HTTP server. + engine.stop() + self.assertEqual(engine.state, engine.states.STOPPED) + + # Verify that our custom stop function was called + self.assertEqual(db_connection.running, False) + self.assertEqual(len(db_connection.threads), 0) + + # Block the main thread now and verify that exit() works. + def exittest(): + self.getPage("/") + self.assertBody("Hello World") + engine.exit() + cherrypy.server.start() + engine.start_with_callback(exittest) + engine.block() + self.assertEqual(engine.state, engine.states.EXITING) + + def test_1_Restart(self): + cherrypy.server.start() + engine.start() + + # The db_connection should be running now + self.assertEqual(db_connection.running, True) + grace = db_connection.gracecount + + self.getPage("/") + self.assertBody("Hello World") + self.assertEqual(len(db_connection.threads), 1) + + # Test server restart from this thread + engine.graceful() + self.assertEqual(engine.state, engine.states.STARTED) + self.getPage("/") + self.assertBody("Hello World") + self.assertEqual(db_connection.running, True) + self.assertEqual(db_connection.gracecount, grace + 1) + self.assertEqual(len(db_connection.threads), 1) + + # Test server restart from inside a page handler + self.getPage("/graceful") + self.assertEqual(engine.state, engine.states.STARTED) + self.assertBody("app was (gracefully) restarted succesfully") + self.assertEqual(db_connection.running, True) + self.assertEqual(db_connection.gracecount, grace + 2) + # Since we are requesting synchronously, is only one thread used? + # Note that the "/graceful" request has been flushed. + self.assertEqual(len(db_connection.threads), 0) + + engine.stop() + self.assertEqual(engine.state, engine.states.STOPPED) + self.assertEqual(db_connection.running, False) + self.assertEqual(len(db_connection.threads), 0) + + def test_2_KeyboardInterrupt(self): + # Raise a keyboard interrupt in the HTTP server's main thread. + # We must start the server in this, the main thread + engine.start() + cherrypy.server.start() + + self.persistent = True + try: + # Make the first request and assert there's no "Connection: close". + self.getPage("/") + self.assertStatus('200 OK') + self.assertBody("Hello World") + self.assertNoHeader("Connection") + + cherrypy.server.httpserver.interrupt = KeyboardInterrupt + engine.block() + + self.assertEqual(db_connection.running, False) + self.assertEqual(len(db_connection.threads), 0) + self.assertEqual(engine.state, engine.states.EXITING) + finally: + self.persistent = False + + # Raise a keyboard interrupt in a page handler; on multithreaded + # servers, this should occur in one of the worker threads. + # This should raise a BadStatusLine error, since the worker + # thread will just die without writing a response. + engine.start() + cherrypy.server.start() + + try: + self.getPage("/ctrlc") + except BadStatusLine: + pass + else: + print(self.body) + self.fail("AssertionError: BadStatusLine not raised") + + engine.block() + self.assertEqual(db_connection.running, False) + self.assertEqual(len(db_connection.threads), 0) + + def test_3_Deadlocks(self): + cherrypy.config.update({'response.timeout': 0.2}) + + engine.start() + cherrypy.server.start() + try: + self.assertNotEqual(engine.timeout_monitor.thread, None) + + # Request a "normal" page. + self.assertEqual(engine.timeout_monitor.servings, []) + self.getPage("/") + self.assertBody("Hello World") + # request.close is called async. + while engine.timeout_monitor.servings: + sys.stdout.write(".") + time.sleep(0.01) + + # Request a page that explicitly checks itself for deadlock. + # The deadlock_timeout should be 2 secs. + self.getPage("/block_explicit") + self.assertBody("broken!") + + # Request a page that implicitly breaks deadlock. + # If we deadlock, we want to touch as little code as possible, + # so we won't even call handle_error, just bail ASAP. + self.getPage("/block_implicit") + self.assertStatus(500) + self.assertInBody("raise cherrypy.TimeoutError()") + finally: + engine.exit() + + def test_4_Autoreload(self): + # Start the demo script in a new process + p = helper.CPProcess(ssl=(self.scheme.lower()=='https')) + p.write_conf( + extra='test_case_name: "test_4_Autoreload"') + p.start(imports='cherrypy.test._test_states_demo') + try: + self.getPage("/start") + start = float(self.body) + + # Give the autoreloader time to cache the file time. + time.sleep(2) + + # Touch the file + os.utime(os.path.join(thisdir, "_test_states_demo.py"), None) + + # Give the autoreloader time to re-exec the process + time.sleep(2) + host = cherrypy.server.socket_host + port = cherrypy.server.socket_port + cherrypy._cpserver.wait_for_occupied_port(host, port) + + self.getPage("/start") + if not (float(self.body) > start): + raise AssertionError("start time %s not greater than %s" % + (float(self.body), start)) + finally: + # Shut down the spawned process + self.getPage("/exit") + p.join() + + def test_5_Start_Error(self): + # If a process errors during start, it should stop the engine + # and exit with a non-zero exit code. + p = helper.CPProcess(ssl=(self.scheme.lower()=='https'), + wait=True) + p.write_conf( + extra="""starterror: True +test_case_name: "test_5_Start_Error" +""" + ) + p.start(imports='cherrypy.test._test_states_demo') + if p.exit_code == 0: + self.fail("Process failed to return nonzero exit code.") + + +class PluginTests(helper.CPWebCase): + def test_daemonize(self): + if os.name not in ['posix']: + return self.skip("skipped (not on posix) ") + self.HOST = '127.0.0.1' + self.PORT = 8081 + # Spawn the process and wait, when this returns, the original process + # is finished. If it daemonized properly, we should still be able + # to access pages. + p = helper.CPProcess(ssl=(self.scheme.lower()=='https'), + wait=True, daemonize=True, + socket_host='127.0.0.1', + socket_port=8081) + p.write_conf( + extra='test_case_name: "test_daemonize"') + p.start(imports='cherrypy.test._test_states_demo') + try: + # Just get the pid of the daemonization process. + self.getPage("/pid") + self.assertStatus(200) + page_pid = int(self.body) + self.assertEqual(page_pid, p.get_pid()) + finally: + # Shut down the spawned process + self.getPage("/exit") + p.join() + + # Wait until here to test the exit code because we want to ensure + # that we wait for the daemon to finish running before we fail. + if p.exit_code != 0: + self.fail("Daemonized parent process failed to exit cleanly.") + + +class SignalHandlingTests(helper.CPWebCase): + def test_SIGHUP_tty(self): + # When not daemonized, SIGHUP should shut down the server. + try: + from signal import SIGHUP + except ImportError: + return self.skip("skipped (no SIGHUP) ") + + # Spawn the process. + p = helper.CPProcess(ssl=(self.scheme.lower()=='https')) + p.write_conf( + extra='test_case_name: "test_SIGHUP_tty"') + p.start(imports='cherrypy.test._test_states_demo') + # Send a SIGHUP + os.kill(p.get_pid(), SIGHUP) + # This might hang if things aren't working right, but meh. + p.join() + + def test_SIGHUP_daemonized(self): + # When daemonized, SIGHUP should restart the server. + try: + from signal import SIGHUP + except ImportError: + return self.skip("skipped (no SIGHUP) ") + + if os.name not in ['posix']: + return self.skip("skipped (not on posix) ") + + # Spawn the process and wait, when this returns, the original process + # is finished. If it daemonized properly, we should still be able + # to access pages. + p = helper.CPProcess(ssl=(self.scheme.lower()=='https'), + wait=True, daemonize=True) + p.write_conf( + extra='test_case_name: "test_SIGHUP_daemonized"') + p.start(imports='cherrypy.test._test_states_demo') + + pid = p.get_pid() + try: + # Send a SIGHUP + os.kill(pid, SIGHUP) + # Give the server some time to restart + time.sleep(2) + self.getPage("/pid") + self.assertStatus(200) + new_pid = int(self.body) + self.assertNotEqual(new_pid, pid) + finally: + # Shut down the spawned process + self.getPage("/exit") + p.join() + + def test_SIGTERM(self): + # SIGTERM should shut down the server whether daemonized or not. + try: + from signal import SIGTERM + except ImportError: + return self.skip("skipped (no SIGTERM) ") + + try: + from os import kill + except ImportError: + return self.skip("skipped (no os.kill) ") + + # Spawn a normal, undaemonized process. + p = helper.CPProcess(ssl=(self.scheme.lower()=='https')) + p.write_conf( + extra='test_case_name: "test_SIGTERM"') + p.start(imports='cherrypy.test._test_states_demo') + # Send a SIGTERM + os.kill(p.get_pid(), SIGTERM) + # This might hang if things aren't working right, but meh. + p.join() + + if os.name in ['posix']: + # Spawn a daemonized process and test again. + p = helper.CPProcess(ssl=(self.scheme.lower()=='https'), + wait=True, daemonize=True) + p.write_conf( + extra='test_case_name: "test_SIGTERM_2"') + p.start(imports='cherrypy.test._test_states_demo') + # Send a SIGTERM + os.kill(p.get_pid(), SIGTERM) + # This might hang if things aren't working right, but meh. + p.join() + + def test_signal_handler_unsubscribe(self): + try: + from signal import SIGTERM + except ImportError: + return self.skip("skipped (no SIGTERM) ") + + try: + from os import kill + except ImportError: + return self.skip("skipped (no os.kill) ") + + # Spawn a normal, undaemonized process. + p = helper.CPProcess(ssl=(self.scheme.lower()=='https')) + p.write_conf( + extra="""unsubsig: True +test_case_name: "test_signal_handler_unsubscribe" +""") + p.start(imports='cherrypy.test._test_states_demo') + # Send a SIGTERM + os.kill(p.get_pid(), SIGTERM) + # This might hang if things aren't working right, but meh. + p.join() + + # Assert the old handler ran. + target_line = open(p.error_log, 'rb').readlines()[-10] + if not ntob("I am an old SIGTERM handler.") in target_line: + self.fail("Old SIGTERM handler did not run.\n%r" % target_line) + diff --git a/libs/cherrypy/test/test_static.py b/libs/cherrypy/test/test_static.py new file mode 100644 index 0000000..871420b --- /dev/null +++ b/libs/cherrypy/test/test_static.py @@ -0,0 +1,300 @@ +from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob +from cherrypy._cpcompat import BytesIO + +import os +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) +has_space_filepath = os.path.join(curdir, 'static', 'has space.html') +bigfile_filepath = os.path.join(curdir, "static", "bigfile.log") +BIGFILE_SIZE = 1024 * 1024 +import threading + +import cherrypy +from cherrypy.lib import static +from cherrypy.test import helper + + +class StaticTest(helper.CPWebCase): + + def setup_server(): + if not os.path.exists(has_space_filepath): + open(has_space_filepath, 'wb').write(ntob('Hello, world\r\n')) + if not os.path.exists(bigfile_filepath): + open(bigfile_filepath, 'wb').write(ntob("x" * BIGFILE_SIZE)) + + class Root: + + def bigfile(self): + from cherrypy.lib import static + self.f = static.serve_file(bigfile_filepath) + return self.f + bigfile.exposed = True + bigfile._cp_config = {'response.stream': True} + + def tell(self): + if self.f.input.closed: + return '' + return repr(self.f.input.tell()).rstrip('L') + tell.exposed = True + + def fileobj(self): + f = open(os.path.join(curdir, 'style.css'), 'rb') + return static.serve_fileobj(f, content_type='text/css') + fileobj.exposed = True + + def bytesio(self): + f = BytesIO(ntob('Fee\nfie\nfo\nfum')) + return static.serve_fileobj(f, content_type='text/plain') + bytesio.exposed = True + + class Static: + + def index(self): + return 'You want the Baron? You can have the Baron!' + index.exposed = True + + def dynamic(self): + return "This is a DYNAMIC page" + dynamic.exposed = True + + + root = Root() + root.static = Static() + + rootconf = { + '/static': { + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.root': curdir, + }, + '/style.css': { + 'tools.staticfile.on': True, + 'tools.staticfile.filename': os.path.join(curdir, 'style.css'), + }, + '/docroot': { + 'tools.staticdir.on': True, + 'tools.staticdir.root': curdir, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.index': 'index.html', + }, + '/error': { + 'tools.staticdir.on': True, + 'request.show_tracebacks': True, + }, + } + rootApp = cherrypy.Application(root) + rootApp.merge(rootconf) + + test_app_conf = { + '/test': { + 'tools.staticdir.index': 'index.html', + 'tools.staticdir.on': True, + 'tools.staticdir.root': curdir, + 'tools.staticdir.dir': 'static', + }, + } + testApp = cherrypy.Application(Static()) + testApp.merge(test_app_conf) + + vhost = cherrypy._cpwsgi.VirtualHost(rootApp, {'virt.net': testApp}) + cherrypy.tree.graft(vhost) + setup_server = staticmethod(setup_server) + + + def teardown_server(): + for f in (has_space_filepath, bigfile_filepath): + if os.path.exists(f): + try: + os.unlink(f) + except: + pass + teardown_server = staticmethod(teardown_server) + + + def testStatic(self): + self.getPage("/static/index.html") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html') + self.assertBody('Hello, world\r\n') + + # Using a staticdir.root value in a subdir... + self.getPage("/docroot/index.html") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html') + self.assertBody('Hello, world\r\n') + + # Check a filename with spaces in it + self.getPage("/static/has%20space.html") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html') + self.assertBody('Hello, world\r\n') + + self.getPage("/style.css") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/css') + # Note: The body should be exactly 'Dummy stylesheet\n', but + # unfortunately some tools such as WinZip sometimes turn \n + # into \r\n on Windows when extracting the CherryPy tarball so + # we just check the content + self.assertMatchesBody('^Dummy stylesheet') + + def test_fallthrough(self): + # Test that NotFound will then try dynamic handlers (see [878]). + self.getPage("/static/dynamic") + self.assertBody("This is a DYNAMIC page") + + # Check a directory via fall-through to dynamic handler. + self.getPage("/static/") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.assertBody('You want the Baron? You can have the Baron!') + + def test_index(self): + # Check a directory via "staticdir.index". + self.getPage("/docroot/") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html') + self.assertBody('Hello, world\r\n') + # The same page should be returned even if redirected. + self.getPage("/docroot") + self.assertStatus(301) + self.assertHeader('Location', '%s/docroot/' % self.base()) + self.assertMatchesBody("This resource .* " + "%s/docroot/." % (self.base(), self.base())) + + def test_config_errors(self): + # Check that we get an error if no .file or .dir + self.getPage("/error/thing.html") + self.assertErrorPage(500) + self.assertMatchesBody(ntob("TypeError: staticdir\(\) takes at least 2 " + "(positional )?arguments \(0 given\)")) + + def test_security(self): + # Test up-level security + self.getPage("/static/../../test/style.css") + self.assertStatus((400, 403)) + + def test_modif(self): + # Test modified-since on a reasonably-large file + self.getPage("/static/dirback.jpg") + self.assertStatus("200 OK") + lastmod = "" + for k, v in self.headers: + if k == 'Last-Modified': + lastmod = v + ims = ("If-Modified-Since", lastmod) + self.getPage("/static/dirback.jpg", headers=[ims]) + self.assertStatus(304) + self.assertNoHeader("Content-Type") + self.assertNoHeader("Content-Length") + self.assertNoHeader("Content-Disposition") + self.assertBody("") + + def test_755_vhost(self): + self.getPage("/test/", [('Host', 'virt.net')]) + self.assertStatus(200) + self.getPage("/test", [('Host', 'virt.net')]) + self.assertStatus(301) + self.assertHeader('Location', self.scheme + '://virt.net/test/') + + def test_serve_fileobj(self): + self.getPage("/fileobj") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/css;charset=utf-8') + self.assertMatchesBody('^Dummy stylesheet') + + def test_serve_bytesio(self): + self.getPage("/bytesio") + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/plain;charset=utf-8') + self.assertHeader('Content-Length', 14) + self.assertMatchesBody('Fee\nfie\nfo\nfum') + + def test_file_stream(self): + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + # Make an initial request + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest("GET", "/bigfile", skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.assertEqual(response.status, 200) + + body = ntob('') + remaining = BIGFILE_SIZE + while remaining > 0: + data = response.fp.read(65536) + if not data: + break + body += data + remaining -= len(data) + + if self.scheme == "https": + newconn = HTTPSConnection + else: + newconn = HTTPConnection + s, h, b = helper.webtest.openURL( + ntob("/tell"), headers=[], host=self.HOST, port=self.PORT, + http_conn=newconn) + if not b: + # The file was closed on the server. + tell_position = BIGFILE_SIZE + else: + tell_position = int(b) + + expected = len(body) + if tell_position >= BIGFILE_SIZE: + # We can't exactly control how much content the server asks for. + # Fudge it by only checking the first half of the reads. + if expected < (BIGFILE_SIZE / 2): + self.fail( + "The file should have advanced to position %r, but has " + "already advanced to the end of the file. It may not be " + "streamed as intended, or at the wrong chunk size (64k)" % + expected) + elif tell_position < expected: + self.fail( + "The file should have advanced to position %r, but has " + "only advanced to position %r. It may not be streamed " + "as intended, or at the wrong chunk size (65536)" % + (expected, tell_position)) + + if body != ntob("x" * BIGFILE_SIZE): + self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." % + (BIGFILE_SIZE, body[:50], len(body))) + conn.close() + + def test_file_stream_deadlock(self): + if cherrypy.server.protocol_version != "HTTP/1.1": + return self.skip() + + self.PROTOCOL = "HTTP/1.1" + + # Make an initial request but abort early. + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest("GET", "/bigfile", skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method="GET") + response.begin() + self.assertEqual(response.status, 200) + body = response.fp.read(65536) + if body != ntob("x" * len(body)): + self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." % + (65536, body[:50], len(body))) + response.close() + conn.close() + + # Make a second request, which should fetch the whole file. + self.persistent = False + self.getPage("/bigfile") + if self.body != ntob("x" * BIGFILE_SIZE): + self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." % + (BIGFILE_SIZE, self.body[:50], len(body))) + diff --git a/libs/cherrypy/test/test_tools.py b/libs/cherrypy/test/test_tools.py new file mode 100644 index 0000000..02bacda --- /dev/null +++ b/libs/cherrypy/test/test_tools.py @@ -0,0 +1,399 @@ +"""Test the various means of instantiating and invoking tools.""" + +import gzip +import sys +from cherrypy._cpcompat import BytesIO, copyitems, itervalues +from cherrypy._cpcompat import IncompleteRead, ntob, ntou, py3k, xrange +import time +timeout = 0.2 +import types + +import cherrypy +from cherrypy import tools + + +europoundUnicode = ntou('\x80\xa3') + + +# Client-side code # + +from cherrypy.test import helper + + +class ToolTests(helper.CPWebCase): + def setup_server(): + + # Put check_access in a custom toolbox with its own namespace + myauthtools = cherrypy._cptools.Toolbox("myauth") + + def check_access(default=False): + if not getattr(cherrypy.request, "userid", default): + raise cherrypy.HTTPError(401) + myauthtools.check_access = cherrypy.Tool('before_request_body', check_access) + + def numerify(): + def number_it(body): + for chunk in body: + for k, v in cherrypy.request.numerify_map: + chunk = chunk.replace(k, v) + yield chunk + cherrypy.response.body = number_it(cherrypy.response.body) + + class NumTool(cherrypy.Tool): + def _setup(self): + def makemap(): + m = self._merged_args().get("map", {}) + cherrypy.request.numerify_map = copyitems(m) + cherrypy.request.hooks.attach('on_start_resource', makemap) + + def critical(): + cherrypy.request.error_response = cherrypy.HTTPError(502).set_response + critical.failsafe = True + + cherrypy.request.hooks.attach('on_start_resource', critical) + cherrypy.request.hooks.attach(self._point, self.callable) + + tools.numerify = NumTool('before_finalize', numerify) + + # It's not mandatory to inherit from cherrypy.Tool. + class NadsatTool: + + def __init__(self): + self.ended = {} + self._name = "nadsat" + + def nadsat(self): + def nadsat_it_up(body): + for chunk in body: + chunk = chunk.replace(ntob("good"), ntob("horrorshow")) + chunk = chunk.replace(ntob("piece"), ntob("lomtick")) + yield chunk + cherrypy.response.body = nadsat_it_up(cherrypy.response.body) + nadsat.priority = 0 + + def cleanup(self): + # This runs after the request has been completely written out. + cherrypy.response.body = [ntob("razdrez")] + id = cherrypy.request.params.get("id") + if id: + self.ended[id] = True + cleanup.failsafe = True + + def _setup(self): + cherrypy.request.hooks.attach('before_finalize', self.nadsat) + cherrypy.request.hooks.attach('on_end_request', self.cleanup) + tools.nadsat = NadsatTool() + + def pipe_body(): + cherrypy.request.process_request_body = False + clen = int(cherrypy.request.headers['Content-Length']) + cherrypy.request.body = cherrypy.request.rfile.read(clen) + + # Assert that we can use a callable object instead of a function. + class Rotator(object): + def __call__(self, scale): + r = cherrypy.response + r.collapse_body() + if py3k: + r.body = [bytes([(x + scale) % 256 for x in r.body[0]])] + else: + r.body = [chr((ord(x) + scale) % 256) for x in r.body[0]] + cherrypy.tools.rotator = cherrypy.Tool('before_finalize', Rotator()) + + def stream_handler(next_handler, *args, **kwargs): + cherrypy.response.output = o = BytesIO() + try: + response = next_handler(*args, **kwargs) + # Ignore the response and return our accumulated output instead. + return o.getvalue() + finally: + o.close() + cherrypy.tools.streamer = cherrypy._cptools.HandlerWrapperTool(stream_handler) + + class Root: + def index(self): + return "Howdy earth!" + index.exposed = True + + def tarfile(self): + cherrypy.response.output.write(ntob('I am ')) + cherrypy.response.output.write(ntob('a tarfile')) + tarfile.exposed = True + tarfile._cp_config = {'tools.streamer.on': True} + + def euro(self): + hooks = list(cherrypy.request.hooks['before_finalize']) + hooks.sort() + cbnames = [x.callback.__name__ for x in hooks] + assert cbnames == ['gzip'], cbnames + priorities = [x.priority for x in hooks] + assert priorities == [80], priorities + yield ntou("Hello,") + yield ntou("world") + yield europoundUnicode + euro.exposed = True + + # Bare hooks + def pipe(self): + return cherrypy.request.body + pipe.exposed = True + pipe._cp_config = {'hooks.before_request_body': pipe_body} + + # Multiple decorators; include kwargs just for fun. + # Note that rotator must run before gzip. + def decorated_euro(self, *vpath): + yield ntou("Hello,") + yield ntou("world") + yield europoundUnicode + decorated_euro.exposed = True + decorated_euro = tools.gzip(compress_level=6)(decorated_euro) + decorated_euro = tools.rotator(scale=3)(decorated_euro) + + root = Root() + + + class TestType(type): + """Metaclass which automatically exposes all functions in each subclass, + and adds an instance of the subclass as an attribute of root. + """ + def __init__(cls, name, bases, dct): + type.__init__(cls, name, bases, dct) + for value in itervalues(dct): + if isinstance(value, types.FunctionType): + value.exposed = True + setattr(root, name.lower(), cls()) + Test = TestType('Test', (object,), {}) + + + # METHOD ONE: + # Declare Tools in _cp_config + class Demo(Test): + + _cp_config = {"tools.nadsat.on": True} + + def index(self, id=None): + return "A good piece of cherry pie" + + def ended(self, id): + return repr(tools.nadsat.ended[id]) + + def err(self, id=None): + raise ValueError() + + def errinstream(self, id=None): + yield "nonconfidential" + raise ValueError() + yield "confidential" + + # METHOD TWO: decorator using Tool() + # We support Python 2.3, but the @-deco syntax would look like this: + # @tools.check_access() + def restricted(self): + return "Welcome!" + restricted = myauthtools.check_access()(restricted) + userid = restricted + + def err_in_onstart(self): + return "success!" + + def stream(self, id=None): + for x in xrange(100000000): + yield str(x) + stream._cp_config = {'response.stream': True} + + + conf = { + # METHOD THREE: + # Declare Tools in detached config + '/demo': { + 'tools.numerify.on': True, + 'tools.numerify.map': {ntob("pie"): ntob("3.14159")}, + }, + '/demo/restricted': { + 'request.show_tracebacks': False, + }, + '/demo/userid': { + 'request.show_tracebacks': False, + 'myauth.check_access.default': True, + }, + '/demo/errinstream': { + 'response.stream': True, + }, + '/demo/err_in_onstart': { + # Because this isn't a dict, on_start_resource will error. + 'tools.numerify.map': "pie->3.14159" + }, + # Combined tools + '/euro': { + 'tools.gzip.on': True, + 'tools.encode.on': True, + }, + # Priority specified in config + '/decorated_euro/subpath': { + 'tools.gzip.priority': 10, + }, + # Handler wrappers + '/tarfile': {'tools.streamer.on': True} + } + app = cherrypy.tree.mount(root, config=conf) + app.request_class.namespaces['myauth'] = myauthtools + + if sys.version_info >= (2, 5): + from cherrypy.test import _test_decorators + root.tooldecs = _test_decorators.ToolExamples() + setup_server = staticmethod(setup_server) + + def testHookErrors(self): + self.getPage("/demo/?id=1") + # If body is "razdrez", then on_end_request is being called too early. + self.assertBody("A horrorshow lomtick of cherry 3.14159") + # If this fails, then on_end_request isn't being called at all. + time.sleep(0.1) + self.getPage("/demo/ended/1") + self.assertBody("True") + + valerr = '\n raise ValueError()\nValueError' + self.getPage("/demo/err?id=3") + # If body is "razdrez", then on_end_request is being called too early. + self.assertErrorPage(502, pattern=valerr) + # If this fails, then on_end_request isn't being called at all. + time.sleep(0.1) + self.getPage("/demo/ended/3") + self.assertBody("True") + + # If body is "razdrez", then on_end_request is being called too early. + if (cherrypy.server.protocol_version == "HTTP/1.0" or + getattr(cherrypy.server, "using_apache", False)): + self.getPage("/demo/errinstream?id=5") + # Because this error is raised after the response body has + # started, the status should not change to an error status. + self.assertStatus("200 OK") + self.assertBody("nonconfidential") + else: + # Because this error is raised after the response body has + # started, and because it's chunked output, an error is raised by + # the HTTP client when it encounters incomplete output. + self.assertRaises((ValueError, IncompleteRead), self.getPage, + "/demo/errinstream?id=5") + # If this fails, then on_end_request isn't being called at all. + time.sleep(0.1) + self.getPage("/demo/ended/5") + self.assertBody("True") + + # Test the "__call__" technique (compile-time decorator). + self.getPage("/demo/restricted") + self.assertErrorPage(401) + + # Test compile-time decorator with kwargs from config. + self.getPage("/demo/userid") + self.assertBody("Welcome!") + + def testEndRequestOnDrop(self): + old_timeout = None + try: + httpserver = cherrypy.server.httpserver + old_timeout = httpserver.timeout + except (AttributeError, IndexError): + return self.skip() + + try: + httpserver.timeout = timeout + + # Test that on_end_request is called even if the client drops. + self.persistent = True + try: + conn = self.HTTP_CONN + conn.putrequest("GET", "/demo/stream?id=9", skip_host=True) + conn.putheader("Host", self.HOST) + conn.endheaders() + # Skip the rest of the request and close the conn. This will + # cause the server's active socket to error, which *should* + # result in the request being aborted, and request.close being + # called all the way up the stack (including WSGI middleware), + # eventually calling our on_end_request hook. + finally: + self.persistent = False + time.sleep(timeout * 2) + # Test that the on_end_request hook was called. + self.getPage("/demo/ended/9") + self.assertBody("True") + finally: + if old_timeout is not None: + httpserver.timeout = old_timeout + + def testGuaranteedHooks(self): + # The 'critical' on_start_resource hook is 'failsafe' (guaranteed + # to run even if there are failures in other on_start methods). + # This is NOT true of the other hooks. + # Here, we have set up a failure in NumerifyTool.numerify_map, + # but our 'critical' hook should run and set the error to 502. + self.getPage("/demo/err_in_onstart") + self.assertErrorPage(502) + self.assertInBody("AttributeError: 'str' object has no attribute 'items'") + + def testCombinedTools(self): + expectedResult = (ntou("Hello,world") + europoundUnicode).encode('utf-8') + zbuf = BytesIO() + zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9) + zfile.write(expectedResult) + zfile.close() + + self.getPage("/euro", headers=[("Accept-Encoding", "gzip"), + ("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7")]) + self.assertInBody(zbuf.getvalue()[:3]) + + zbuf = BytesIO() + zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=6) + zfile.write(expectedResult) + zfile.close() + + self.getPage("/decorated_euro", headers=[("Accept-Encoding", "gzip")]) + self.assertInBody(zbuf.getvalue()[:3]) + + # This returns a different value because gzip's priority was + # lowered in conf, allowing the rotator to run after gzip. + # Of course, we don't want breakage in production apps, + # but it proves the priority was changed. + self.getPage("/decorated_euro/subpath", + headers=[("Accept-Encoding", "gzip")]) + if py3k: + self.assertInBody(bytes([(x + 3) % 256 for x in zbuf.getvalue()])) + else: + self.assertInBody(''.join([chr((ord(x) + 3) % 256) for x in zbuf.getvalue()])) + + def testBareHooks(self): + content = "bit of a pain in me gulliver" + self.getPage("/pipe", + headers=[("Content-Length", str(len(content))), + ("Content-Type", "text/plain")], + method="POST", body=content) + self.assertBody(content) + + def testHandlerWrapperTool(self): + self.getPage("/tarfile") + self.assertBody("I am a tarfile") + + def testToolWithConfig(self): + if not sys.version_info >= (2, 5): + return self.skip("skipped (Python 2.5+ only)") + + self.getPage('/tooldecs/blah') + self.assertHeader('Content-Type', 'application/data') + + def testWarnToolOn(self): + # get + try: + numon = cherrypy.tools.numerify.on + except AttributeError: + pass + else: + raise AssertionError("Tool.on did not error as it should have.") + + # set + try: + cherrypy.tools.numerify.on = True + except AttributeError: + pass + else: + raise AssertionError("Tool.on did not error as it should have.") + diff --git a/libs/cherrypy/test/test_tutorials.py b/libs/cherrypy/test/test_tutorials.py new file mode 100644 index 0000000..aab2786 --- /dev/null +++ b/libs/cherrypy/test/test_tutorials.py @@ -0,0 +1,201 @@ +import sys + +import cherrypy +from cherrypy.test import helper + + +class TutorialTest(helper.CPWebCase): + + def setup_server(cls): + + conf = cherrypy.config.copy() + + def load_tut_module(name): + """Import or reload tutorial module as needed.""" + cherrypy.config.reset() + cherrypy.config.update(conf) + + target = "cherrypy.tutorial." + name + if target in sys.modules: + module = reload(sys.modules[target]) + else: + module = __import__(target, globals(), locals(), ['']) + # The above import will probably mount a new app at "". + app = cherrypy.tree.apps[""] + + app.root.load_tut_module = load_tut_module + app.root.sessions = sessions + app.root.traceback_setting = traceback_setting + + cls.supervisor.sync_apps() + load_tut_module.exposed = True + + def sessions(): + cherrypy.config.update({"tools.sessions.on": True}) + sessions.exposed = True + + def traceback_setting(): + return repr(cherrypy.request.show_tracebacks) + traceback_setting.exposed = True + + class Dummy: + pass + root = Dummy() + root.load_tut_module = load_tut_module + cherrypy.tree.mount(root) + setup_server = classmethod(setup_server) + + + def test01HelloWorld(self): + self.getPage("/load_tut_module/tut01_helloworld") + self.getPage("/") + self.assertBody('Hello world!') + + def test02ExposeMethods(self): + self.getPage("/load_tut_module/tut02_expose_methods") + self.getPage("/showMessage") + self.assertBody('Hello world!') + + def test03GetAndPost(self): + self.getPage("/load_tut_module/tut03_get_and_post") + + # Try different GET queries + self.getPage("/greetUser?name=Bob") + self.assertBody("Hey Bob, what's up?") + + self.getPage("/greetUser") + self.assertBody('Please enter your name here.') + + self.getPage("/greetUser?name=") + self.assertBody('No, really, enter your name here.') + + # Try the same with POST + self.getPage("/greetUser", method="POST", body="name=Bob") + self.assertBody("Hey Bob, what's up?") + + self.getPage("/greetUser", method="POST", body="name=") + self.assertBody('No, really, enter your name here.') + + def test04ComplexSite(self): + self.getPage("/load_tut_module/tut04_complex_site") + msg = ''' +

Here are some extra useful links:

+ + + +

[Return to links page]

''' + self.getPage("/links/extra/") + self.assertBody(msg) + + def test05DerivedObjects(self): + self.getPage("/load_tut_module/tut05_derived_objects") + msg = ''' + + + Another Page + + +

Another Page

+ +

+ And this is the amazing second page! +

+ + + + ''' + self.getPage("/another/") + self.assertBody(msg) + + def test06DefaultMethod(self): + self.getPage("/load_tut_module/tut06_default_method") + self.getPage('/hendrik') + self.assertBody('Hendrik Mans, CherryPy co-developer & crazy German ' + '(back)') + + def test07Sessions(self): + self.getPage("/load_tut_module/tut07_sessions") + self.getPage("/sessions") + + self.getPage('/') + self.assertBody("\n During your current session, you've viewed this" + "\n page 1 times! Your life is a patio of fun!" + "\n ") + + self.getPage('/', self.cookies) + self.assertBody("\n During your current session, you've viewed this" + "\n page 2 times! Your life is a patio of fun!" + "\n ") + + def test08GeneratorsAndYield(self): + self.getPage("/load_tut_module/tut08_generators_and_yield") + self.getPage('/') + self.assertBody('

Generators rule!

' + '

List of users:

' + 'Remi
Carlos
Hendrik
Lorenzo Lamas
' + '') + + def test09Files(self): + self.getPage("/load_tut_module/tut09_files") + + # Test upload + filesize = 5 + h = [("Content-type", "multipart/form-data; boundary=x"), + ("Content-Length", str(105 + filesize))] + b = '--x\n' + \ + 'Content-Disposition: form-data; name="myFile"; filename="hello.txt"\r\n' + \ + 'Content-Type: text/plain\r\n' + \ + '\r\n' + \ + 'a' * filesize + '\n' + \ + '--x--\n' + self.getPage('/upload', h, "POST", b) + self.assertBody(''' + + myFile length: %d
+ myFile filename: hello.txt
+ myFile mime-type: text/plain + + ''' % filesize) + + # Test download + self.getPage('/download') + self.assertStatus("200 OK") + self.assertHeader("Content-Type", "application/x-download") + self.assertHeader("Content-Disposition", + # Make sure the filename is quoted. + 'attachment; filename="pdf_file.pdf"') + self.assertEqual(len(self.body), 85698) + + def test10HTTPErrors(self): + self.getPage("/load_tut_module/tut10_http_errors") + + self.getPage("/") + self.assertInBody("""""") + self.assertInBody("""""") + self.assertInBody("""""") + self.assertInBody("""""") + self.assertInBody("""""") + + self.getPage("/traceback_setting") + setting = self.body + self.getPage("/toggleTracebacks") + self.assertStatus((302, 303)) + self.getPage("/traceback_setting") + self.assertBody(str(not eval(setting))) + + self.getPage("/error?code=500") + self.assertStatus(500) + self.assertInBody("The server encountered an unexpected condition " + "which prevented it from fulfilling the request.") + + self.getPage("/error?code=403") + self.assertStatus(403) + self.assertInBody("

You can't do that!

") + + self.getPage("/messageArg") + self.assertStatus(500) + self.assertInBody("If you construct an HTTPError with a 'message'") + diff --git a/libs/cherrypy/test/test_virtualhost.py b/libs/cherrypy/test/test_virtualhost.py new file mode 100644 index 0000000..dbd2dbc --- /dev/null +++ b/libs/cherrypy/test/test_virtualhost.py @@ -0,0 +1,107 @@ +import os +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + +import cherrypy +from cherrypy.test import helper + + +class VirtualHostTest(helper.CPWebCase): + + def setup_server(): + class Root: + def index(self): + return "Hello, world" + index.exposed = True + + def dom4(self): + return "Under construction" + dom4.exposed = True + + def method(self, value): + return "You sent %s" % value + method.exposed = True + + class VHost: + def __init__(self, sitename): + self.sitename = sitename + + def index(self): + return "Welcome to %s" % self.sitename + index.exposed = True + + def vmethod(self, value): + return "You sent %s" % value + vmethod.exposed = True + + def url(self): + return cherrypy.url("nextpage") + url.exposed = True + + # Test static as a handler (section must NOT include vhost prefix) + static = cherrypy.tools.staticdir.handler(section='/static', dir=curdir) + + root = Root() + root.mydom2 = VHost("Domain 2") + root.mydom3 = VHost("Domain 3") + hostmap = {'www.mydom2.com': '/mydom2', + 'www.mydom3.com': '/mydom3', + 'www.mydom4.com': '/dom4', + } + cherrypy.tree.mount(root, config={ + '/': {'request.dispatch': cherrypy.dispatch.VirtualHost(**hostmap)}, + # Test static in config (section must include vhost prefix) + '/mydom2/static2': {'tools.staticdir.on': True, + 'tools.staticdir.root': curdir, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.index': 'index.html', + }, + }) + setup_server = staticmethod(setup_server) + + def testVirtualHost(self): + self.getPage("/", [('Host', 'www.mydom1.com')]) + self.assertBody('Hello, world') + self.getPage("/mydom2/", [('Host', 'www.mydom1.com')]) + self.assertBody('Welcome to Domain 2') + + self.getPage("/", [('Host', 'www.mydom2.com')]) + self.assertBody('Welcome to Domain 2') + self.getPage("/", [('Host', 'www.mydom3.com')]) + self.assertBody('Welcome to Domain 3') + self.getPage("/", [('Host', 'www.mydom4.com')]) + self.assertBody('Under construction') + + # Test GET, POST, and positional params + self.getPage("/method?value=root") + self.assertBody("You sent root") + self.getPage("/vmethod?value=dom2+GET", [('Host', 'www.mydom2.com')]) + self.assertBody("You sent dom2 GET") + self.getPage("/vmethod", [('Host', 'www.mydom3.com')], method="POST", + body="value=dom3+POST") + self.assertBody("You sent dom3 POST") + self.getPage("/vmethod/pos", [('Host', 'www.mydom3.com')]) + self.assertBody("You sent pos") + + # Test that cherrypy.url uses the browser url, not the virtual url + self.getPage("/url", [('Host', 'www.mydom2.com')]) + self.assertBody("%s://www.mydom2.com/nextpage" % self.scheme) + + def test_VHost_plus_Static(self): + # Test static as a handler + self.getPage("/static/style.css", [('Host', 'www.mydom2.com')]) + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/css;charset=utf-8') + + # Test static in config + self.getPage("/static2/dirback.jpg", [('Host', 'www.mydom2.com')]) + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'image/jpeg') + + # Test static config with "index" arg + self.getPage("/static2/", [('Host', 'www.mydom2.com')]) + self.assertStatus('200 OK') + self.assertBody('Hello, world\r\n') + # Since tools.trailing_slash is on by default, this should redirect + self.getPage("/static2", [('Host', 'www.mydom2.com')]) + self.assertStatus(301) + diff --git a/libs/cherrypy/test/test_wsgi_ns.py b/libs/cherrypy/test/test_wsgi_ns.py new file mode 100644 index 0000000..e3c6ce6 --- /dev/null +++ b/libs/cherrypy/test/test_wsgi_ns.py @@ -0,0 +1,91 @@ +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy.test import helper + + +class WSGI_Namespace_Test(helper.CPWebCase): + + def setup_server(): + + class WSGIResponse(object): + + def __init__(self, appresults): + self.appresults = appresults + self.iter = iter(appresults) + + def __iter__(self): + return self + + def next(self): + return self.iter.next() + def __next__(self): + return next(self.iter) + + def close(self): + if hasattr(self.appresults, "close"): + self.appresults.close() + + + class ChangeCase(object): + + def __init__(self, app, to=None): + self.app = app + self.to = to + + def __call__(self, environ, start_response): + res = self.app(environ, start_response) + class CaseResults(WSGIResponse): + def next(this): + return getattr(this.iter.next(), self.to)() + def __next__(this): + return getattr(next(this.iter), self.to)() + return CaseResults(res) + + class Replacer(object): + + def __init__(self, app, map={}): + self.app = app + self.map = map + + def __call__(self, environ, start_response): + res = self.app(environ, start_response) + class ReplaceResults(WSGIResponse): + def next(this): + line = this.iter.next() + for k, v in self.map.iteritems(): + line = line.replace(k, v) + return line + def __next__(this): + line = next(this.iter) + for k, v in self.map.items(): + line = line.replace(k, v) + return line + return ReplaceResults(res) + + class Root(object): + + def index(self): + return "HellO WoRlD!" + index.exposed = True + + + root_conf = {'wsgi.pipeline': [('replace', Replacer)], + 'wsgi.replace.map': {ntob('L'): ntob('X'), + ntob('l'): ntob('r')}, + } + + app = cherrypy.Application(Root()) + app.wsgiapp.pipeline.append(('changecase', ChangeCase)) + app.wsgiapp.config['changecase'] = {'to': 'upper'} + cherrypy.tree.mount(app, config={'/': root_conf}) + setup_server = staticmethod(setup_server) + + + def test_pipeline(self): + if not cherrypy.server.httpserver: + return self.skip() + + self.getPage("/") + # If body is "HEXXO WORXD!", the middleware was applied out of order. + self.assertBody("HERRO WORRD!") + diff --git a/libs/cherrypy/test/test_wsgi_vhost.py b/libs/cherrypy/test/test_wsgi_vhost.py new file mode 100644 index 0000000..abb1a91 --- /dev/null +++ b/libs/cherrypy/test/test_wsgi_vhost.py @@ -0,0 +1,36 @@ +import cherrypy +from cherrypy.test import helper + + +class WSGI_VirtualHost_Test(helper.CPWebCase): + + def setup_server(): + + class ClassOfRoot(object): + + def __init__(self, name): + self.name = name + + def index(self): + return "Welcome to the %s website!" % self.name + index.exposed = True + + + default = cherrypy.Application(None) + + domains = {} + for year in range(1997, 2008): + app = cherrypy.Application(ClassOfRoot('Class of %s' % year)) + domains['www.classof%s.example' % year] = app + + cherrypy.tree.graft(cherrypy._cpwsgi.VirtualHost(default, domains)) + setup_server = staticmethod(setup_server) + + def test_welcome(self): + if not cherrypy.server.using_wsgi: + return self.skip("skipped (not using WSGI)... ") + + for year in range(1997, 2008): + self.getPage("/", headers=[('Host', 'www.classof%s.example' % year)]) + self.assertBody("Welcome to the Class of %s website!" % year) + diff --git a/libs/cherrypy/test/test_wsgiapps.py b/libs/cherrypy/test/test_wsgiapps.py new file mode 100644 index 0000000..d4b8b79 --- /dev/null +++ b/libs/cherrypy/test/test_wsgiapps.py @@ -0,0 +1,118 @@ +from cherrypy._cpcompat import ntob +from cherrypy.test import helper + + +class WSGIGraftTests(helper.CPWebCase): + + def setup_server(): + import os + curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + import cherrypy + + def test_app(environ, start_response): + status = '200 OK' + response_headers = [('Content-type', 'text/plain')] + start_response(status, response_headers) + output = ['Hello, world!\n', + 'This is a wsgi app running within CherryPy!\n\n'] + keys = list(environ.keys()) + keys.sort() + for k in keys: + output.append('%s: %s\n' % (k,environ[k])) + return [ntob(x, 'utf-8') for x in output] + + def test_empty_string_app(environ, start_response): + status = '200 OK' + response_headers = [('Content-type', 'text/plain')] + start_response(status, response_headers) + return [ntob('Hello'), ntob(''), ntob(' '), ntob(''), ntob('world')] + + + class WSGIResponse(object): + + def __init__(self, appresults): + self.appresults = appresults + self.iter = iter(appresults) + + def __iter__(self): + return self + + def next(self): + return self.iter.next() + def __next__(self): + return next(self.iter) + + def close(self): + if hasattr(self.appresults, "close"): + self.appresults.close() + + + class ReversingMiddleware(object): + + def __init__(self, app): + self.app = app + + def __call__(self, environ, start_response): + results = app(environ, start_response) + class Reverser(WSGIResponse): + def next(this): + line = list(this.iter.next()) + line.reverse() + return "".join(line) + def __next__(this): + line = list(next(this.iter)) + line.reverse() + return bytes(line) + return Reverser(results) + + class Root: + def index(self): + return ntob("I'm a regular CherryPy page handler!") + index.exposed = True + + + cherrypy.tree.mount(Root()) + + cherrypy.tree.graft(test_app, '/hosted/app1') + cherrypy.tree.graft(test_empty_string_app, '/hosted/app3') + + # Set script_name explicitly to None to signal CP that it should + # be pulled from the WSGI environ each time. + app = cherrypy.Application(Root(), script_name=None) + cherrypy.tree.graft(ReversingMiddleware(app), '/hosted/app2') + setup_server = staticmethod(setup_server) + + wsgi_output = '''Hello, world! +This is a wsgi app running within CherryPy!''' + + def test_01_standard_app(self): + self.getPage("/") + self.assertBody("I'm a regular CherryPy page handler!") + + def test_04_pure_wsgi(self): + import cherrypy + if not cherrypy.server.using_wsgi: + return self.skip("skipped (not using WSGI)... ") + self.getPage("/hosted/app1") + self.assertHeader("Content-Type", "text/plain") + self.assertInBody(self.wsgi_output) + + def test_05_wrapped_cp_app(self): + import cherrypy + if not cherrypy.server.using_wsgi: + return self.skip("skipped (not using WSGI)... ") + self.getPage("/hosted/app2/") + body = list("I'm a regular CherryPy page handler!") + body.reverse() + body = "".join(body) + self.assertInBody(body) + + def test_06_empty_string_app(self): + import cherrypy + if not cherrypy.server.using_wsgi: + return self.skip("skipped (not using WSGI)... ") + self.getPage("/hosted/app3") + self.assertHeader("Content-Type", "text/plain") + self.assertInBody('Hello world') + diff --git a/libs/cherrypy/test/test_xmlrpc.py b/libs/cherrypy/test/test_xmlrpc.py new file mode 100644 index 0000000..f7a6927 --- /dev/null +++ b/libs/cherrypy/test/test_xmlrpc.py @@ -0,0 +1,179 @@ +import sys +from cherrypy._cpcompat import py3k + +try: + from xmlrpclib import DateTime, Fault, ProtocolError, ServerProxy, SafeTransport +except ImportError: + from xmlrpc.client import DateTime, Fault, ProtocolError, ServerProxy, SafeTransport + +if py3k: + HTTPSTransport = SafeTransport + + # Python 3.0's SafeTransport still mistakenly checks for socket.ssl + import socket + if not hasattr(socket, "ssl"): + socket.ssl = True +else: + class HTTPSTransport(SafeTransport): + """Subclass of SafeTransport to fix sock.recv errors (by using file).""" + + def request(self, host, handler, request_body, verbose=0): + # issue XML-RPC request + h = self.make_connection(host) + if verbose: + h.set_debuglevel(1) + + self.send_request(h, handler, request_body) + self.send_host(h, host) + self.send_user_agent(h) + self.send_content(h, request_body) + + errcode, errmsg, headers = h.getreply() + if errcode != 200: + raise ProtocolError(host + handler, errcode, errmsg, headers) + + self.verbose = verbose + + # Here's where we differ from the superclass. It says: + # try: + # sock = h._conn.sock + # except AttributeError: + # sock = None + # return self._parse_response(h.getfile(), sock) + + return self.parse_response(h.getfile()) + +import cherrypy + + +def setup_server(): + from cherrypy import _cptools + + class Root: + def index(self): + return "I'm a standard index!" + index.exposed = True + + + class XmlRpc(_cptools.XMLRPCController): + + def foo(self): + return "Hello world!" + foo.exposed = True + + def return_single_item_list(self): + return [42] + return_single_item_list.exposed = True + + def return_string(self): + return "here is a string" + return_string.exposed = True + + def return_tuple(self): + return ('here', 'is', 1, 'tuple') + return_tuple.exposed = True + + def return_dict(self): + return dict(a=1, b=2, c=3) + return_dict.exposed = True + + def return_composite(self): + return dict(a=1,z=26), 'hi', ['welcome', 'friend'] + return_composite.exposed = True + + def return_int(self): + return 42 + return_int.exposed = True + + def return_float(self): + return 3.14 + return_float.exposed = True + + def return_datetime(self): + return DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1)) + return_datetime.exposed = True + + def return_boolean(self): + return True + return_boolean.exposed = True + + def test_argument_passing(self, num): + return num * 2 + test_argument_passing.exposed = True + + def test_returning_Fault(self): + return Fault(1, "custom Fault response") + test_returning_Fault.exposed = True + + root = Root() + root.xmlrpc = XmlRpc() + cherrypy.tree.mount(root, config={'/': { + 'request.dispatch': cherrypy.dispatch.XMLRPCDispatcher(), + 'tools.xmlrpc.allow_none': 0, + }}) + + +from cherrypy.test import helper + +class XmlRpcTest(helper.CPWebCase): + setup_server = staticmethod(setup_server) + def testXmlRpc(self): + + scheme = self.scheme + if scheme == "https": + url = 'https://%s:%s/xmlrpc/' % (self.interface(), self.PORT) + proxy = ServerProxy(url, transport=HTTPSTransport()) + else: + url = 'http://%s:%s/xmlrpc/' % (self.interface(), self.PORT) + proxy = ServerProxy(url) + + # begin the tests ... + self.getPage("/xmlrpc/foo") + self.assertBody("Hello world!") + + self.assertEqual(proxy.return_single_item_list(), [42]) + self.assertNotEqual(proxy.return_single_item_list(), 'one bazillion') + self.assertEqual(proxy.return_string(), "here is a string") + self.assertEqual(proxy.return_tuple(), list(('here', 'is', 1, 'tuple'))) + self.assertEqual(proxy.return_dict(), {'a': 1, 'c': 3, 'b': 2}) + self.assertEqual(proxy.return_composite(), + [{'a': 1, 'z': 26}, 'hi', ['welcome', 'friend']]) + self.assertEqual(proxy.return_int(), 42) + self.assertEqual(proxy.return_float(), 3.14) + self.assertEqual(proxy.return_datetime(), + DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1))) + self.assertEqual(proxy.return_boolean(), True) + self.assertEqual(proxy.test_argument_passing(22), 22 * 2) + + # Test an error in the page handler (should raise an xmlrpclib.Fault) + try: + proxy.test_argument_passing({}) + except Exception: + x = sys.exc_info()[1] + self.assertEqual(x.__class__, Fault) + self.assertEqual(x.faultString, ("unsupported operand type(s) " + "for *: 'dict' and 'int'")) + else: + self.fail("Expected xmlrpclib.Fault") + + # http://www.cherrypy.org/ticket/533 + # if a method is not found, an xmlrpclib.Fault should be raised + try: + proxy.non_method() + except Exception: + x = sys.exc_info()[1] + self.assertEqual(x.__class__, Fault) + self.assertEqual(x.faultString, 'method "non_method" is not supported') + else: + self.fail("Expected xmlrpclib.Fault") + + # Test returning a Fault from the page handler. + try: + proxy.test_returning_Fault() + except Exception: + x = sys.exc_info()[1] + self.assertEqual(x.__class__, Fault) + self.assertEqual(x.faultString, ("custom Fault response")) + else: + self.fail("Expected xmlrpclib.Fault") + diff --git a/libs/cherrypy/test/webtest.py b/libs/cherrypy/test/webtest.py new file mode 100644 index 0000000..50cfbad --- /dev/null +++ b/libs/cherrypy/test/webtest.py @@ -0,0 +1,575 @@ +"""Extensions to unittest for web frameworks. + +Use the WebCase.getPage method to request a page from your HTTP server. + +Framework Integration +===================== + +If you have control over your server process, you can handle errors +in the server-side of the HTTP conversation a bit better. You must run +both the client (your WebCase tests) and the server in the same process +(but in separate threads, obviously). + +When an error occurs in the framework, call server_error. It will print +the traceback to stdout, and keep any assertions you have from running +(the assumption is that, if the server errors, the page output will not +be of further significance to your tests). +""" + +import os +import pprint +import re +import socket +import sys +import time +import traceback +import types + +from unittest import * +from unittest import _TextTestResult + +from cherrypy._cpcompat import basestring, ntob, py3k, HTTPConnection, HTTPSConnection, unicodestr + + + +def interface(host): + """Return an IP address for a client connection given the server host. + + If the server is listening on '0.0.0.0' (INADDR_ANY) + or '::' (IN6ADDR_ANY), this will return the proper localhost.""" + if host == '0.0.0.0': + # INADDR_ANY, which should respond on localhost. + return "127.0.0.1" + if host == '::': + # IN6ADDR_ANY, which should respond on localhost. + return "::1" + return host + + +class TerseTestResult(_TextTestResult): + + def printErrors(self): + # Overridden to avoid unnecessary empty line + if self.errors or self.failures: + if self.dots or self.showAll: + self.stream.writeln() + self.printErrorList('ERROR', self.errors) + self.printErrorList('FAIL', self.failures) + + +class TerseTestRunner(TextTestRunner): + """A test runner class that displays results in textual form.""" + + def _makeResult(self): + return TerseTestResult(self.stream, self.descriptions, self.verbosity) + + def run(self, test): + "Run the given test case or test suite." + # Overridden to remove unnecessary empty lines and separators + result = self._makeResult() + test(result) + result.printErrors() + if not result.wasSuccessful(): + self.stream.write("FAILED (") + failed, errored = list(map(len, (result.failures, result.errors))) + if failed: + self.stream.write("failures=%d" % failed) + if errored: + if failed: self.stream.write(", ") + self.stream.write("errors=%d" % errored) + self.stream.writeln(")") + return result + + +class ReloadingTestLoader(TestLoader): + + def loadTestsFromName(self, name, module=None): + """Return a suite of all tests cases given a string specifier. + + The name may resolve either to a module, a test case class, a + test method within a test case class, or a callable object which + returns a TestCase or TestSuite instance. + + The method optionally resolves the names relative to a given module. + """ + parts = name.split('.') + unused_parts = [] + if module is None: + if not parts: + raise ValueError("incomplete test name: %s" % name) + else: + parts_copy = parts[:] + while parts_copy: + target = ".".join(parts_copy) + if target in sys.modules: + module = reload(sys.modules[target]) + parts = unused_parts + break + else: + try: + module = __import__(target) + parts = unused_parts + break + except ImportError: + unused_parts.insert(0,parts_copy[-1]) + del parts_copy[-1] + if not parts_copy: + raise + parts = parts[1:] + obj = module + for part in parts: + obj = getattr(obj, part) + + if type(obj) == types.ModuleType: + return self.loadTestsFromModule(obj) + elif (((py3k and isinstance(obj, type)) + or isinstance(obj, (type, types.ClassType))) + and issubclass(obj, TestCase)): + return self.loadTestsFromTestCase(obj) + elif type(obj) == types.UnboundMethodType: + if py3k: + return obj.__self__.__class__(obj.__name__) + else: + return obj.im_class(obj.__name__) + elif hasattr(obj, '__call__'): + test = obj() + if not isinstance(test, TestCase) and \ + not isinstance(test, TestSuite): + raise ValueError("calling %s returned %s, " + "not a test" % (obj,test)) + return test + else: + raise ValueError("do not know how to make test from: %s" % obj) + + +try: + # Jython support + if sys.platform[:4] == 'java': + def getchar(): + # Hopefully this is enough + return sys.stdin.read(1) + else: + # On Windows, msvcrt.getch reads a single char without output. + import msvcrt + def getchar(): + return msvcrt.getch() +except ImportError: + # Unix getchr + import tty, termios + def getchar(): + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch + + +class WebCase(TestCase): + HOST = "127.0.0.1" + PORT = 8000 + HTTP_CONN = HTTPConnection + PROTOCOL = "HTTP/1.1" + + scheme = "http" + url = None + + status = None + headers = None + body = None + + encoding = 'utf-8' + + time = None + + def get_conn(self, auto_open=False): + """Return a connection to our HTTP server.""" + if self.scheme == "https": + cls = HTTPSConnection + else: + cls = HTTPConnection + conn = cls(self.interface(), self.PORT) + # Automatically re-connect? + conn.auto_open = auto_open + conn.connect() + return conn + + def set_persistent(self, on=True, auto_open=False): + """Make our HTTP_CONN persistent (or not). + + If the 'on' argument is True (the default), then self.HTTP_CONN + will be set to an instance of HTTPConnection (or HTTPS + if self.scheme is "https"). This will then persist across requests. + + We only allow for a single open connection, so if you call this + and we currently have an open connection, it will be closed. + """ + try: + self.HTTP_CONN.close() + except (TypeError, AttributeError): + pass + + if on: + self.HTTP_CONN = self.get_conn(auto_open=auto_open) + else: + if self.scheme == "https": + self.HTTP_CONN = HTTPSConnection + else: + self.HTTP_CONN = HTTPConnection + + def _get_persistent(self): + return hasattr(self.HTTP_CONN, "__class__") + def _set_persistent(self, on): + self.set_persistent(on) + persistent = property(_get_persistent, _set_persistent) + + def interface(self): + """Return an IP address for a client connection. + + If the server is listening on '0.0.0.0' (INADDR_ANY) + or '::' (IN6ADDR_ANY), this will return the proper localhost.""" + return interface(self.HOST) + + def getPage(self, url, headers=None, method="GET", body=None, protocol=None): + """Open the url with debugging support. Return status, headers, body.""" + ServerError.on = False + + if isinstance(url, unicodestr): + url = url.encode('utf-8') + if isinstance(body, unicodestr): + body = body.encode('utf-8') + + self.url = url + self.time = None + start = time.time() + result = openURL(url, headers, method, body, self.HOST, self.PORT, + self.HTTP_CONN, protocol or self.PROTOCOL) + self.time = time.time() - start + self.status, self.headers, self.body = result + + # Build a list of request cookies from the previous response cookies. + self.cookies = [('Cookie', v) for k, v in self.headers + if k.lower() == 'set-cookie'] + + if ServerError.on: + raise ServerError() + return result + + interactive = True + console_height = 30 + + def _handlewebError(self, msg): + print("") + print(" ERROR: %s" % msg) + + if not self.interactive: + raise self.failureException(msg) + + p = " Show: [B]ody [H]eaders [S]tatus [U]RL; [I]gnore, [R]aise, or sys.e[X]it >> " + sys.stdout.write(p) + sys.stdout.flush() + while True: + i = getchar().upper() + if not isinstance(i, type("")): + i = i.decode('ascii') + if i not in "BHSUIRX": + continue + print(i.upper()) # Also prints new line + if i == "B": + for x, line in enumerate(self.body.splitlines()): + if (x + 1) % self.console_height == 0: + # The \r and comma should make the next line overwrite + sys.stdout.write("<-- More -->\r") + m = getchar().lower() + # Erase our "More" prompt + sys.stdout.write(" \r") + if m == "q": + break + print(line) + elif i == "H": + pprint.pprint(self.headers) + elif i == "S": + print(self.status) + elif i == "U": + print(self.url) + elif i == "I": + # return without raising the normal exception + return + elif i == "R": + raise self.failureException(msg) + elif i == "X": + self.exit() + sys.stdout.write(p) + sys.stdout.flush() + + def exit(self): + sys.exit() + + def assertStatus(self, status, msg=None): + """Fail if self.status != status.""" + if isinstance(status, basestring): + if not self.status == status: + if msg is None: + msg = 'Status (%r) != %r' % (self.status, status) + self._handlewebError(msg) + elif isinstance(status, int): + code = int(self.status[:3]) + if code != status: + if msg is None: + msg = 'Status (%r) != %r' % (self.status, status) + self._handlewebError(msg) + else: + # status is a tuple or list. + match = False + for s in status: + if isinstance(s, basestring): + if self.status == s: + match = True + break + elif int(self.status[:3]) == s: + match = True + break + if not match: + if msg is None: + msg = 'Status (%r) not in %r' % (self.status, status) + self._handlewebError(msg) + + def assertHeader(self, key, value=None, msg=None): + """Fail if (key, [value]) not in self.headers.""" + lowkey = key.lower() + for k, v in self.headers: + if k.lower() == lowkey: + if value is None or str(value) == v: + return v + + if msg is None: + if value is None: + msg = '%r not in headers' % key + else: + msg = '%r:%r not in headers' % (key, value) + self._handlewebError(msg) + + def assertHeaderItemValue(self, key, value, msg=None): + """Fail if the header does not contain the specified value""" + actual_value = self.assertHeader(key, msg=msg) + header_values = map(str.strip, actual_value.split(',')) + if value in header_values: + return value + + if msg is None: + msg = "%r not in %r" % (value, header_values) + self._handlewebError(msg) + + def assertNoHeader(self, key, msg=None): + """Fail if key in self.headers.""" + lowkey = key.lower() + matches = [k for k, v in self.headers if k.lower() == lowkey] + if matches: + if msg is None: + msg = '%r in headers' % key + self._handlewebError(msg) + + def assertBody(self, value, msg=None): + """Fail if value != self.body.""" + if isinstance(value, unicodestr): + value = value.encode(self.encoding) + if value != self.body: + if msg is None: + msg = 'expected body:\n%r\n\nactual body:\n%r' % (value, self.body) + self._handlewebError(msg) + + def assertInBody(self, value, msg=None): + """Fail if value not in self.body.""" + if isinstance(value, unicodestr): + value = value.encode(self.encoding) + if value not in self.body: + if msg is None: + msg = '%r not in body: %s' % (value, self.body) + self._handlewebError(msg) + + def assertNotInBody(self, value, msg=None): + """Fail if value in self.body.""" + if isinstance(value, unicodestr): + value = value.encode(self.encoding) + if value in self.body: + if msg is None: + msg = '%r found in body' % value + self._handlewebError(msg) + + def assertMatchesBody(self, pattern, msg=None, flags=0): + """Fail if value (a regex pattern) is not in self.body.""" + if isinstance(pattern, unicodestr): + pattern = pattern.encode(self.encoding) + if re.search(pattern, self.body, flags) is None: + if msg is None: + msg = 'No match for %r in body' % pattern + self._handlewebError(msg) + + +methods_with_bodies = ("POST", "PUT") + +def cleanHeaders(headers, method, body, host, port): + """Return request headers, with required headers added (if missing).""" + if headers is None: + headers = [] + + # Add the required Host request header if not present. + # [This specifies the host:port of the server, not the client.] + found = False + for k, v in headers: + if k.lower() == 'host': + found = True + break + if not found: + if port == 80: + headers.append(("Host", host)) + else: + headers.append(("Host", "%s:%s" % (host, port))) + + if method in methods_with_bodies: + # Stick in default type and length headers if not present + found = False + for k, v in headers: + if k.lower() == 'content-type': + found = True + break + if not found: + headers.append(("Content-Type", "application/x-www-form-urlencoded")) + headers.append(("Content-Length", str(len(body or "")))) + + return headers + + +def shb(response): + """Return status, headers, body the way we like from a response.""" + if py3k: + h = response.getheaders() + else: + h = [] + key, value = None, None + for line in response.msg.headers: + if line: + if line[0] in " \t": + value += line.strip() + else: + if key and value: + h.append((key, value)) + key, value = line.split(":", 1) + key = key.strip() + value = value.strip() + if key and value: + h.append((key, value)) + + return "%s %s" % (response.status, response.reason), h, response.read() + + +def openURL(url, headers=None, method="GET", body=None, + host="127.0.0.1", port=8000, http_conn=HTTPConnection, + protocol="HTTP/1.1"): + """Open the given HTTP resource and return status, headers, and body.""" + + headers = cleanHeaders(headers, method, body, host, port) + + # Trying 10 times is simply in case of socket errors. + # Normal case--it should run once. + for trial in range(10): + try: + # Allow http_conn to be a class or an instance + if hasattr(http_conn, "host"): + conn = http_conn + else: + conn = http_conn(interface(host), port) + + conn._http_vsn_str = protocol + conn._http_vsn = int("".join([x for x in protocol if x.isdigit()])) + + # skip_accept_encoding argument added in python version 2.4 + if sys.version_info < (2, 4): + def putheader(self, header, value): + if header == 'Accept-Encoding' and value == 'identity': + return + self.__class__.putheader(self, header, value) + import new + conn.putheader = new.instancemethod(putheader, conn, conn.__class__) + conn.putrequest(method.upper(), url, skip_host=True) + elif not py3k: + conn.putrequest(method.upper(), url, skip_host=True, + skip_accept_encoding=True) + else: + import http.client + # Replace the stdlib method, which only accepts ASCII url's + def putrequest(self, method, url): + if self._HTTPConnection__response and self._HTTPConnection__response.isclosed(): + self._HTTPConnection__response = None + + if self._HTTPConnection__state == http.client._CS_IDLE: + self._HTTPConnection__state = http.client._CS_REQ_STARTED + else: + raise http.client.CannotSendRequest() + + self._method = method + if not url: + url = ntob('/') + request = ntob(' ').join((method.encode("ASCII"), url, + self._http_vsn_str.encode("ASCII"))) + self._output(request) + import types + conn.putrequest = types.MethodType(putrequest, conn) + + conn.putrequest(method.upper(), url) + + for key, value in headers: + conn.putheader(key, ntob(value, "Latin-1")) + conn.endheaders() + + if body is not None: + conn.send(body) + + # Handle response + response = conn.getresponse() + + s, h, b = shb(response) + + if not hasattr(http_conn, "host"): + # We made our own conn instance. Close it. + conn.close() + + return s, h, b + except socket.error: + time.sleep(0.5) + if trial == 9: + raise + + +# Add any exceptions which your web framework handles +# normally (that you don't want server_error to trap). +ignored_exceptions = [] + +# You'll want set this to True when you can't guarantee +# that each response will immediately follow each request; +# for example, when handling requests via multiple threads. +ignore_all = False + +class ServerError(Exception): + on = False + + +def server_error(exc=None): + """Server debug hook. Return True if exception handled, False if ignored. + + You probably want to wrap this, so you can still handle an error using + your framework when it's ignored. + """ + if exc is None: + exc = sys.exc_info() + + if ignore_all or exc[0] in ignored_exceptions: + return False + else: + ServerError.on = True + print("") + print("".join(traceback.format_exception(*exc))) + return True + diff --git a/libs/cherrypy/tutorial/README.txt b/libs/cherrypy/tutorial/README.txt new file mode 100644 index 0000000..2b877e1 --- /dev/null +++ b/libs/cherrypy/tutorial/README.txt @@ -0,0 +1,16 @@ +CherryPy Tutorials +------------------------------------------------------------------------ + +This is a series of tutorials explaining how to develop dynamic web +applications using CherryPy. A couple of notes: + + - Each of these tutorials builds on the ones before it. If you're + new to CherryPy, we recommend you start with 01_helloworld.py and + work your way upwards. :) + + - In most of these tutorials, you will notice that all output is done + by returning normal Python strings, often using simple Python + variable substitution. In most real-world applications, you will + probably want to use a separate template package (like Cheetah, + CherryTemplate or XML/XSL). + diff --git a/libs/cherrypy/tutorial/__init__.py b/libs/cherrypy/tutorial/__init__.py new file mode 100644 index 0000000..c4e2c55 --- /dev/null +++ b/libs/cherrypy/tutorial/__init__.py @@ -0,0 +1,3 @@ + +# This is used in test_config to test unrepr of "from A import B" +thing2 = object() \ No newline at end of file diff --git a/libs/cherrypy/tutorial/bonus-sqlobject.py b/libs/cherrypy/tutorial/bonus-sqlobject.py new file mode 100644 index 0000000..c43feb4 --- /dev/null +++ b/libs/cherrypy/tutorial/bonus-sqlobject.py @@ -0,0 +1,168 @@ +''' +Bonus Tutorial: Using SQLObject + +This is a silly little contacts manager application intended to +demonstrate how to use SQLObject from within a CherryPy2 project. It +also shows how to use inline Cheetah templates. + +SQLObject is an Object/Relational Mapper that allows you to access +data stored in an RDBMS in a pythonic fashion. You create data objects +as Python classes and let SQLObject take care of all the nasty details. + +This code depends on the latest development version (0.6+) of SQLObject. +You can get it from the SQLObject Subversion server. You can find all +necessary information at . This code will NOT +work with the 0.5.x version advertised on their website! + +This code also depends on a recent version of Cheetah. You can find +Cheetah at . + +After starting this application for the first time, you will need to +access the /reset URI in order to create the database table and some +sample data. Accessing /reset again will drop and re-create the table, +so you may want to be careful. :-) + +This application isn't supposed to be fool-proof, it's not even supposed +to be very GOOD. Play around with it some, browse the source code, smile. + +:) + +-- Hendrik Mans +''' + +import cherrypy +from Cheetah.Template import Template +from sqlobject import * + +# configure your database connection here +__connection__ = 'mysql://root:@localhost/test' + +# this is our (only) data class. +class Contact(SQLObject): + lastName = StringCol(length = 50, notNone = True) + firstName = StringCol(length = 50, notNone = True) + phone = StringCol(length = 30, notNone = True, default = '') + email = StringCol(length = 30, notNone = True, default = '') + url = StringCol(length = 100, notNone = True, default = '') + + +class ContactManager: + def index(self): + # Let's display a list of all stored contacts. + contacts = Contact.select() + + template = Template(''' +

All Contacts

+ + #for $contact in $contacts +
$contact.lastName, $contact.firstName + [Edit] + [Delete] +
+ #end for + +

[Add new contact]

+ ''', [locals(), globals()]) + + return template.respond() + + index.exposed = True + + + def edit(self, id = 0): + # we really want id as an integer. Since GET/POST parameters + # are always passed as strings, let's convert it. + id = int(id) + + if id > 0: + # if an id is specified, we're editing an existing contact. + contact = Contact.get(id) + title = "Edit Contact" + else: + # if no id is specified, we're entering a new contact. + contact = None + title = "New Contact" + + + # In the following template code, please note that we use + # Cheetah's $getVar() construct for the form values. We have + # to do this because contact may be set to None (see above). + template = Template(''' +

$title

+ + + + Last Name:
+ First Name:
+ Phone:
+ Email:
+ URL:
+ +
+ ''', [locals(), globals()]) + + return template.respond() + + edit.exposed = True + + + def delete(self, id): + # Delete the specified contact + contact = Contact.get(int(id)) + contact.destroySelf() + return 'Deleted. Return to Index' + + delete.exposed = True + + + def store(self, lastName, firstName, phone, email, url, id = None): + if id and int(id) > 0: + # If an id was specified, update an existing contact. + contact = Contact.get(int(id)) + + # We could set one field after another, but that would + # cause multiple UPDATE clauses. So we'll just do it all + # in a single pass through the set() method. + contact.set( + lastName = lastName, + firstName = firstName, + phone = phone, + email = email, + url = url) + else: + # Otherwise, add a new contact. + contact = Contact( + lastName = lastName, + firstName = firstName, + phone = phone, + email = email, + url = url) + + return 'Stored. Return to Index' + + store.exposed = True + + + def reset(self): + # Drop existing table + Contact.dropTable(True) + + # Create new table + Contact.createTable() + + # Create some sample data + Contact( + firstName = 'Hendrik', + lastName = 'Mans', + email = 'hendrik@mans.de', + phone = '++49 89 12345678', + url = 'http://www.mornography.de') + + return "reset completed!" + + reset.exposed = True + + +print("If you're running this application for the first time, please go to http://localhost:8080/reset once in order to create the database!") + +cherrypy.quickstart(ContactManager()) diff --git a/libs/cherrypy/tutorial/custom_error.html b/libs/cherrypy/tutorial/custom_error.html new file mode 100644 index 0000000..d0f30c8 --- /dev/null +++ b/libs/cherrypy/tutorial/custom_error.html @@ -0,0 +1,14 @@ + + + + + 403 Unauthorized + + +

You can't do that!

+

%(message)s

+

This is a custom error page that is read from a file.

+

%(traceback)s
+ + diff --git a/libs/cherrypy/tutorial/pdf_file.pdf b/libs/cherrypy/tutorial/pdf_file.pdf new file mode 100644 index 0000000000000000000000000000000000000000..38b4f15eabdd65d4a674cb32034361245aa7b97e GIT binary patch literal 85698 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 literal 0 HcmV?d00001 diff --git a/libs/cherrypy/tutorial/tut01_helloworld.py b/libs/cherrypy/tutorial/tut01_helloworld.py new file mode 100644 index 0000000..ef94760 --- /dev/null +++ b/libs/cherrypy/tutorial/tut01_helloworld.py @@ -0,0 +1,35 @@ +""" +Tutorial - Hello World + +The most basic (working) CherryPy application possible. +""" + +# Import CherryPy global namespace +import cherrypy + +class HelloWorld: + """ Sample request handler class. """ + + def index(self): + # CherryPy will call this method for the root URI ("/") and send + # its return value to the client. Because this is tutorial + # lesson number 01, we'll just send something really simple. + # How about... + return "Hello world!" + + # Expose the index method through the web. CherryPy will never + # publish methods that don't have the exposed attribute set to True. + index.exposed = True + + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(HelloWorld(), config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(HelloWorld(), config=tutconf) diff --git a/libs/cherrypy/tutorial/tut02_expose_methods.py b/libs/cherrypy/tutorial/tut02_expose_methods.py new file mode 100644 index 0000000..600fca3 --- /dev/null +++ b/libs/cherrypy/tutorial/tut02_expose_methods.py @@ -0,0 +1,32 @@ +""" +Tutorial - Multiple methods + +This tutorial shows you how to link to other methods of your request +handler. +""" + +import cherrypy + +class HelloWorld: + + def index(self): + # Let's link to another method here. + return 'We have an important message for you!' + index.exposed = True + + def showMessage(self): + # Here's the important message! + return "Hello world!" + showMessage.exposed = True + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(HelloWorld(), config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(HelloWorld(), config=tutconf) diff --git a/libs/cherrypy/tutorial/tut03_get_and_post.py b/libs/cherrypy/tutorial/tut03_get_and_post.py new file mode 100644 index 0000000..283477d --- /dev/null +++ b/libs/cherrypy/tutorial/tut03_get_and_post.py @@ -0,0 +1,53 @@ +""" +Tutorial - Passing variables + +This tutorial shows you how to pass GET/POST variables to methods. +""" + +import cherrypy + + +class WelcomePage: + + def index(self): + # Ask for the user's name. + return ''' +
+ What is your name? + + +
''' + index.exposed = True + + def greetUser(self, name = None): + # CherryPy passes all GET and POST variables as method parameters. + # It doesn't make a difference where the variables come from, how + # large their contents are, and so on. + # + # You can define default parameter values as usual. In this + # example, the "name" parameter defaults to None so we can check + # if a name was actually specified. + + if name: + # Greet the user! + return "Hey %s, what's up?" % name + else: + if name is None: + # No name was specified + return 'Please enter your name here.' + else: + return 'No, really, enter your name here.' + greetUser.exposed = True + + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(WelcomePage(), config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(WelcomePage(), config=tutconf) diff --git a/libs/cherrypy/tutorial/tut04_complex_site.py b/libs/cherrypy/tutorial/tut04_complex_site.py new file mode 100644 index 0000000..b4d820e --- /dev/null +++ b/libs/cherrypy/tutorial/tut04_complex_site.py @@ -0,0 +1,98 @@ +""" +Tutorial - Multiple objects + +This tutorial shows you how to create a site structure through multiple +possibly nested request handler objects. +""" + +import cherrypy + + +class HomePage: + def index(self): + return ''' +

Hi, this is the home page! Check out the other + fun stuff on this site:

+ + ''' + index.exposed = True + + +class JokePage: + def index(self): + return ''' +

"In Python, how do you create a string of random + characters?" -- "Read a Perl file!"

+

[Return]

''' + index.exposed = True + + +class LinksPage: + def __init__(self): + # Request handler objects can create their own nested request + # handler objects. Simply create them inside their __init__ + # methods! + self.extra = ExtraLinksPage() + + def index(self): + # Note the way we link to the extra links page (and back). + # As you can see, this object doesn't really care about its + # absolute position in the site tree, since we use relative + # links exclusively. + return ''' +

Here are some useful links:

+ + + +

You can check out some extra useful + links here.

+ +

[Return]

+ ''' + index.exposed = True + + +class ExtraLinksPage: + def index(self): + # Note the relative link back to the Links page! + return ''' +

Here are some extra useful links:

+ + + +

[Return to links page]

''' + index.exposed = True + + +# Of course we can also mount request handler objects right here! +root = HomePage() +root.joke = JokePage() +root.links = LinksPage() + +# Remember, we don't need to mount ExtraLinksPage here, because +# LinksPage does that itself on initialization. In fact, there is +# no reason why you shouldn't let your root object take care of +# creating all contained request handler objects. + + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(root, config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(root, config=tutconf) + diff --git a/libs/cherrypy/tutorial/tut05_derived_objects.py b/libs/cherrypy/tutorial/tut05_derived_objects.py new file mode 100644 index 0000000..3d4ec9b --- /dev/null +++ b/libs/cherrypy/tutorial/tut05_derived_objects.py @@ -0,0 +1,83 @@ +""" +Tutorial - Object inheritance + +You are free to derive your request handler classes from any base +class you wish. In most real-world applications, you will probably +want to create a central base class used for all your pages, which takes +care of things like printing a common page header and footer. +""" + +import cherrypy + + +class Page: + # Store the page title in a class attribute + title = 'Untitled Page' + + def header(self): + return ''' + + + %s + + +

%s

+ ''' % (self.title, self.title) + + def footer(self): + return ''' + + + ''' + + # Note that header and footer don't get their exposed attributes + # set to True. This isn't necessary since the user isn't supposed + # to call header or footer directly; instead, we'll call them from + # within the actually exposed handler methods defined in this + # class' subclasses. + + +class HomePage(Page): + # Different title for this page + title = 'Tutorial 5' + + def __init__(self): + # create a subpage + self.another = AnotherPage() + + def index(self): + # Note that we call the header and footer methods inherited + # from the Page class! + return self.header() + ''' +

+ Isn't this exciting? There's + another page, too! +

+ ''' + self.footer() + index.exposed = True + + +class AnotherPage(Page): + title = 'Another Page' + + def index(self): + return self.header() + ''' +

+ And this is the amazing second page! +

+ ''' + self.footer() + index.exposed = True + + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(HomePage(), config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(HomePage(), config=tutconf) + diff --git a/libs/cherrypy/tutorial/tut06_default_method.py b/libs/cherrypy/tutorial/tut06_default_method.py new file mode 100644 index 0000000..fe24f38 --- /dev/null +++ b/libs/cherrypy/tutorial/tut06_default_method.py @@ -0,0 +1,64 @@ +""" +Tutorial - The default method + +Request handler objects can implement a method called "default" that +is called when no other suitable method/object could be found. +Essentially, if CherryPy2 can't find a matching request handler object +for the given request URI, it will use the default method of the object +located deepest on the URI path. + +Using this mechanism you can easily simulate virtual URI structures +by parsing the extra URI string, which you can access through +cherrypy.request.virtualPath. + +The application in this tutorial simulates an URI structure looking +like /users/. Since the bit will not be found (as +there are no matching methods), it is handled by the default method. +""" + +import cherrypy + + +class UsersPage: + + def index(self): + # Since this is just a stupid little example, we'll simply + # display a list of links to random, made-up users. In a real + # application, this could be generated from a database result set. + return ''' + Remi Delon
+ Hendrik Mans
+ Lorenzo Lamas
+ ''' + index.exposed = True + + def default(self, user): + # Here we react depending on the virtualPath -- the part of the + # path that could not be mapped to an object method. In a real + # application, we would probably do some database lookups here + # instead of the silly if/elif/else construct. + if user == 'remi': + out = "Remi Delon, CherryPy lead developer" + elif user == 'hendrik': + out = "Hendrik Mans, CherryPy co-developer & crazy German" + elif user == 'lorenzo': + out = "Lorenzo Lamas, famous actor and singer!" + else: + out = "Unknown user. :-(" + + return '%s (back)' % out + default.exposed = True + + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(UsersPage(), config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(UsersPage(), config=tutconf) + diff --git a/libs/cherrypy/tutorial/tut07_sessions.py b/libs/cherrypy/tutorial/tut07_sessions.py new file mode 100644 index 0000000..4b1386b --- /dev/null +++ b/libs/cherrypy/tutorial/tut07_sessions.py @@ -0,0 +1,44 @@ +""" +Tutorial - Sessions + +Storing session data in CherryPy applications is very easy: cherrypy +provides a dictionary called "session" that represents the session +data for the current user. If you use RAM based sessions, you can store +any kind of object into that dictionary; otherwise, you are limited to +objects that can be pickled. +""" + +import cherrypy + + +class HitCounter: + + _cp_config = {'tools.sessions.on': True} + + def index(self): + # Increase the silly hit counter + count = cherrypy.session.get('count', 0) + 1 + + # Store the new value in the session dictionary + cherrypy.session['count'] = count + + # And display a silly hit count message! + return ''' + During your current session, you've viewed this + page %s times! Your life is a patio of fun! + ''' % count + index.exposed = True + + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(HitCounter(), config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(HitCounter(), config=tutconf) + diff --git a/libs/cherrypy/tutorial/tut08_generators_and_yield.py b/libs/cherrypy/tutorial/tut08_generators_and_yield.py new file mode 100644 index 0000000..a6fbdc2 --- /dev/null +++ b/libs/cherrypy/tutorial/tut08_generators_and_yield.py @@ -0,0 +1,47 @@ +""" +Bonus Tutorial: Using generators to return result bodies + +Instead of returning a complete result string, you can use the yield +statement to return one result part after another. This may be convenient +in situations where using a template package like CherryPy or Cheetah +would be overkill, and messy string concatenation too uncool. ;-) +""" + +import cherrypy + + +class GeneratorDemo: + + def header(self): + return "

Generators rule!

" + + def footer(self): + return "" + + def index(self): + # Let's make up a list of users for presentation purposes + users = ['Remi', 'Carlos', 'Hendrik', 'Lorenzo Lamas'] + + # Every yield line adds one part to the total result body. + yield self.header() + yield "

List of users:

" + + for user in users: + yield "%s
" % user + + yield self.footer() + index.exposed = True + + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(GeneratorDemo(), config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(GeneratorDemo(), config=tutconf) + diff --git a/libs/cherrypy/tutorial/tut09_files.py b/libs/cherrypy/tutorial/tut09_files.py new file mode 100644 index 0000000..4c8e581 --- /dev/null +++ b/libs/cherrypy/tutorial/tut09_files.py @@ -0,0 +1,107 @@ +""" + +Tutorial: File upload and download + +Uploads +------- + +When a client uploads a file to a CherryPy application, it's placed +on disk immediately. CherryPy will pass it to your exposed method +as an argument (see "myFile" below); that arg will have a "file" +attribute, which is a handle to the temporary uploaded file. +If you wish to permanently save the file, you need to read() +from myFile.file and write() somewhere else. + +Note the use of 'enctype="multipart/form-data"' and 'input type="file"' +in the HTML which the client uses to upload the file. + + +Downloads +--------- + +If you wish to send a file to the client, you have two options: +First, you can simply return a file-like object from your page handler. +CherryPy will read the file and serve it as the content (HTTP body) +of the response. However, that doesn't tell the client that +the response is a file to be saved, rather than displayed. +Use cherrypy.lib.static.serve_file for that; it takes four +arguments: + +serve_file(path, content_type=None, disposition=None, name=None) + +Set "name" to the filename that you expect clients to use when they save +your file. Note that the "name" argument is ignored if you don't also +provide a "disposition" (usually "attachement"). You can manually set +"content_type", but be aware that if you also use the encoding tool, it +may choke if the file extension is not recognized as belonging to a known +Content-Type. Setting the content_type to "application/x-download" works +in most cases, and should prompt the user with an Open/Save dialog in +popular browsers. + +""" + +import os +localDir = os.path.dirname(__file__) +absDir = os.path.join(os.getcwd(), localDir) + +import cherrypy +from cherrypy.lib import static + + +class FileDemo(object): + + def index(self): + return """ + +

Upload a file

+
+ filename:
+ +
+

Download a file

+ This one + + """ + index.exposed = True + + def upload(self, myFile): + out = """ + + myFile length: %s
+ myFile filename: %s
+ myFile mime-type: %s + + """ + + # Although this just counts the file length, it demonstrates + # how to read large files in chunks instead of all at once. + # CherryPy reads the uploaded file into a temporary file; + # myFile.file.read reads from that. + size = 0 + while True: + data = myFile.file.read(8192) + if not data: + break + size += len(data) + + return out % (size, myFile.filename, myFile.content_type) + upload.exposed = True + + def download(self): + path = os.path.join(absDir, "pdf_file.pdf") + return static.serve_file(path, "application/x-download", + "attachment", os.path.basename(path)) + download.exposed = True + + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(FileDemo(), config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(FileDemo(), config=tutconf) diff --git a/libs/cherrypy/tutorial/tut10_http_errors.py b/libs/cherrypy/tutorial/tut10_http_errors.py new file mode 100644 index 0000000..dfa5733 --- /dev/null +++ b/libs/cherrypy/tutorial/tut10_http_errors.py @@ -0,0 +1,81 @@ +""" + +Tutorial: HTTP errors + +HTTPError is used to return an error response to the client. +CherryPy has lots of options regarding how such errors are +logged, displayed, and formatted. + +""" + +import os +localDir = os.path.dirname(__file__) +curpath = os.path.normpath(os.path.join(os.getcwd(), localDir)) + +import cherrypy + + +class HTTPErrorDemo(object): + + # Set a custom response for 403 errors. + _cp_config = {'error_page.403' : os.path.join(curpath, "custom_error.html")} + + def index(self): + # display some links that will result in errors + tracebacks = cherrypy.request.show_tracebacks + if tracebacks: + trace = 'off' + else: + trace = 'on' + + return """ + +

Toggle tracebacks %s

+

Click me; I'm a broken link!

+

Use a custom error page from a file.

+

These errors are explicitly raised by the application:

+ +

You can also set the response body + when you raise an error.

+ + """ % trace + index.exposed = True + + def toggleTracebacks(self): + # simple function to toggle tracebacks on and off + tracebacks = cherrypy.request.show_tracebacks + cherrypy.config.update({'request.show_tracebacks': not tracebacks}) + + # redirect back to the index + raise cherrypy.HTTPRedirect('/') + toggleTracebacks.exposed = True + + def error(self, code): + # raise an error based on the get query + raise cherrypy.HTTPError(status = code) + error.exposed = True + + def messageArg(self): + message = ("If you construct an HTTPError with a 'message' " + "argument, it wil be placed on the error page " + "(underneath the status line by default).") + raise cherrypy.HTTPError(500, message=message) + messageArg.exposed = True + + +import os.path +tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') + +if __name__ == '__main__': + # CherryPy always starts with app.root when trying to map request URIs + # to objects, so we need to mount a request handler root. A request + # to '/' will be mapped to HelloWorld().index(). + cherrypy.quickstart(HTTPErrorDemo(), config=tutconf) +else: + # This branch is for the test suite; you can ignore it. + cherrypy.tree.mount(HTTPErrorDemo(), config=tutconf) diff --git a/libs/cherrypy/tutorial/tutorial.conf b/libs/cherrypy/tutorial/tutorial.conf new file mode 100644 index 0000000..6537fd3 --- /dev/null +++ b/libs/cherrypy/tutorial/tutorial.conf @@ -0,0 +1,4 @@ +[global] +server.socket_host = "127.0.0.1" +server.socket_port = 8080 +server.thread_pool = 10 diff --git a/libs/cherrypy/wsgiserver/__init__.py b/libs/cherrypy/wsgiserver/__init__.py new file mode 100644 index 0000000..ee6190f --- /dev/null +++ b/libs/cherrypy/wsgiserver/__init__.py @@ -0,0 +1,14 @@ +__all__ = ['HTTPRequest', 'HTTPConnection', 'HTTPServer', + 'SizeCheckWrapper', 'KnownLengthRFile', 'ChunkedRFile', + 'MaxSizeExceeded', 'NoSSLError', 'FatalSSLAlert', + 'WorkerThread', 'ThreadPool', 'SSLAdapter', + 'CherryPyWSGIServer', + 'Gateway', 'WSGIGateway', 'WSGIGateway_10', 'WSGIGateway_u0', + 'WSGIPathInfoDispatcher', 'get_ssl_adapter_class'] + +import sys +if sys.version_info < (3, 0): + from wsgiserver2 import * +else: + # Le sigh. Boo for backward-incompatible syntax. + exec('from .wsgiserver3 import *') diff --git a/libs/cherrypy/wsgiserver/ssl_builtin.py b/libs/cherrypy/wsgiserver/ssl_builtin.py new file mode 100644 index 0000000..03bf05d --- /dev/null +++ b/libs/cherrypy/wsgiserver/ssl_builtin.py @@ -0,0 +1,91 @@ +"""A library for integrating Python's builtin ``ssl`` library with CherryPy. + +The ssl module must be importable for SSL functionality. + +To use this module, set ``CherryPyWSGIServer.ssl_adapter`` to an instance of +``BuiltinSSLAdapter``. +""" + +try: + import ssl +except ImportError: + ssl = None + +try: + from _pyio import DEFAULT_BUFFER_SIZE +except ImportError: + try: + from io import DEFAULT_BUFFER_SIZE + except ImportError: + DEFAULT_BUFFER_SIZE = -1 + +import sys + +from cherrypy import wsgiserver + + +class BuiltinSSLAdapter(wsgiserver.SSLAdapter): + """A wrapper for integrating Python's builtin ssl module with CherryPy.""" + + certificate = None + """The filename of the server SSL certificate.""" + + private_key = None + """The filename of the server's private key file.""" + + def __init__(self, certificate, private_key, certificate_chain=None): + if ssl is None: + raise ImportError("You must install the ssl module to use HTTPS.") + self.certificate = certificate + self.private_key = private_key + self.certificate_chain = certificate_chain + + def bind(self, sock): + """Wrap and return the given socket.""" + return sock + + def wrap(self, sock): + """Wrap and return the given socket, plus WSGI environ entries.""" + try: + s = ssl.wrap_socket(sock, do_handshake_on_connect=True, + server_side=True, certfile=self.certificate, + keyfile=self.private_key, ssl_version=ssl.PROTOCOL_SSLv23) + except ssl.SSLError: + e = sys.exc_info()[1] + if e.errno == ssl.SSL_ERROR_EOF: + # This is almost certainly due to the cherrypy engine + # 'pinging' the socket to assert it's connectable; + # the 'ping' isn't SSL. + return None, {} + elif e.errno == ssl.SSL_ERROR_SSL: + if e.args[1].endswith('http request'): + # The client is speaking HTTP to an HTTPS server. + raise wsgiserver.NoSSLError + elif e.args[1].endswith('unknown protocol'): + # The client is speaking some non-HTTP protocol. + # Drop the conn. + return None, {} + raise + return s, self.get_environ(s) + + # TODO: fill this out more with mod ssl env + def get_environ(self, sock): + """Create WSGI environ entries to be merged into each request.""" + cipher = sock.cipher() + ssl_environ = { + "wsgi.url_scheme": "https", + "HTTPS": "on", + 'SSL_PROTOCOL': cipher[1], + 'SSL_CIPHER': cipher[0] +## SSL_VERSION_INTERFACE string The mod_ssl program version +## SSL_VERSION_LIBRARY string The OpenSSL program version + } + return ssl_environ + + if sys.version_info >= (3, 0): + def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE): + return wsgiserver.CP_makefile(sock, mode, bufsize) + else: + def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE): + return wsgiserver.CP_fileobject(sock, mode, bufsize) + diff --git a/libs/cherrypy/wsgiserver/ssl_pyopenssl.py b/libs/cherrypy/wsgiserver/ssl_pyopenssl.py new file mode 100644 index 0000000..f3d9bf5 --- /dev/null +++ b/libs/cherrypy/wsgiserver/ssl_pyopenssl.py @@ -0,0 +1,256 @@ +"""A library for integrating pyOpenSSL with CherryPy. + +The OpenSSL module must be importable for SSL functionality. +You can obtain it from http://pyopenssl.sourceforge.net/ + +To use this module, set CherryPyWSGIServer.ssl_adapter to an instance of +SSLAdapter. There are two ways to use SSL: + +Method One +---------- + + * ``ssl_adapter.context``: an instance of SSL.Context. + +If this is not None, it is assumed to be an SSL.Context instance, +and will be passed to SSL.Connection on bind(). The developer is +responsible for forming a valid Context object. This approach is +to be preferred for more flexibility, e.g. if the cert and key are +streams instead of files, or need decryption, or SSL.SSLv3_METHOD +is desired instead of the default SSL.SSLv23_METHOD, etc. Consult +the pyOpenSSL documentation for complete options. + +Method Two (shortcut) +--------------------- + + * ``ssl_adapter.certificate``: the filename of the server SSL certificate. + * ``ssl_adapter.private_key``: the filename of the server's private key file. + +Both are None by default. If ssl_adapter.context is None, but .private_key +and .certificate are both given and valid, they will be read, and the +context will be automatically created from them. +""" + +import socket +import threading +import time + +from cherrypy import wsgiserver + +try: + from OpenSSL import SSL + from OpenSSL import crypto +except ImportError: + SSL = None + + +class SSL_fileobject(wsgiserver.CP_fileobject): + """SSL file object attached to a socket object.""" + + ssl_timeout = 3 + ssl_retry = .01 + + def _safe_call(self, is_reader, call, *args, **kwargs): + """Wrap the given call with SSL error-trapping. + + is_reader: if False EOF errors will be raised. If True, EOF errors + will return "" (to emulate normal sockets). + """ + start = time.time() + while True: + try: + return call(*args, **kwargs) + except SSL.WantReadError: + # Sleep and try again. This is dangerous, because it means + # the rest of the stack has no way of differentiating + # between a "new handshake" error and "client dropped". + # Note this isn't an endless loop: there's a timeout below. + time.sleep(self.ssl_retry) + except SSL.WantWriteError: + time.sleep(self.ssl_retry) + except SSL.SysCallError, e: + if is_reader and e.args == (-1, 'Unexpected EOF'): + return "" + + errnum = e.args[0] + if is_reader and errnum in wsgiserver.socket_errors_to_ignore: + return "" + raise socket.error(errnum) + except SSL.Error, e: + if is_reader and e.args == (-1, 'Unexpected EOF'): + return "" + + thirdarg = None + try: + thirdarg = e.args[0][0][2] + except IndexError: + pass + + if thirdarg == 'http request': + # The client is talking HTTP to an HTTPS server. + raise wsgiserver.NoSSLError() + + raise wsgiserver.FatalSSLAlert(*e.args) + except: + raise + + if time.time() - start > self.ssl_timeout: + raise socket.timeout("timed out") + + def recv(self, *args, **kwargs): + buf = [] + r = super(SSL_fileobject, self).recv + while True: + data = self._safe_call(True, r, *args, **kwargs) + buf.append(data) + p = self._sock.pending() + if not p: + return "".join(buf) + + def sendall(self, *args, **kwargs): + return self._safe_call(False, super(SSL_fileobject, self).sendall, + *args, **kwargs) + + def send(self, *args, **kwargs): + return self._safe_call(False, super(SSL_fileobject, self).send, + *args, **kwargs) + + +class SSLConnection: + """A thread-safe wrapper for an SSL.Connection. + + ``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``. + """ + + def __init__(self, *args): + self._ssl_conn = SSL.Connection(*args) + self._lock = threading.RLock() + + for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read', + 'renegotiate', 'bind', 'listen', 'connect', 'accept', + 'setblocking', 'fileno', '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', 'gettimeout'): + exec("""def %s(self, *args): + self._lock.acquire() + try: + return self._ssl_conn.%s(*args) + finally: + self._lock.release() +""" % (f, f)) + + def shutdown(self, *args): + self._lock.acquire() + try: + # pyOpenSSL.socket.shutdown takes no args + return self._ssl_conn.shutdown() + finally: + self._lock.release() + + +class pyOpenSSLAdapter(wsgiserver.SSLAdapter): + """A wrapper for integrating pyOpenSSL with CherryPy.""" + + context = None + """An instance of SSL.Context.""" + + certificate = None + """The filename of the server SSL certificate.""" + + private_key = None + """The filename of the server's private key file.""" + + certificate_chain = None + """Optional. The filename of CA's intermediate certificate bundle. + + This is needed for cheaper "chained root" SSL certificates, and should be + left as None if not required.""" + + def __init__(self, certificate, private_key, certificate_chain=None): + if SSL is None: + raise ImportError("You must install pyOpenSSL to use HTTPS.") + + self.context = None + self.certificate = certificate + self.private_key = private_key + self.certificate_chain = certificate_chain + self._environ = None + + def bind(self, sock): + """Wrap and return the given socket.""" + if self.context is None: + self.context = self.get_context() + conn = SSLConnection(self.context, sock) + self._environ = self.get_environ() + return conn + + def wrap(self, sock): + """Wrap and return the given socket, plus WSGI environ entries.""" + return sock, self._environ.copy() + + def get_context(self): + """Return an SSL.Context from self attributes.""" + # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473 + c = SSL.Context(SSL.SSLv23_METHOD) + c.use_privatekey_file(self.private_key) + if self.certificate_chain: + c.load_verify_locations(self.certificate_chain) + c.use_certificate_file(self.certificate) + return c + + def get_environ(self): + """Return WSGI environ entries to be merged into each request.""" + ssl_environ = { + "HTTPS": "on", + # pyOpenSSL doesn't provide access to any of these AFAICT +## 'SSL_PROTOCOL': 'SSLv2', +## SSL_CIPHER string The cipher specification name +## SSL_VERSION_INTERFACE string The mod_ssl program version +## SSL_VERSION_LIBRARY string The OpenSSL program version + } + + if self.certificate: + # Server certificate attributes + cert = open(self.certificate, 'rb').read() + cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert) + ssl_environ.update({ + 'SSL_SERVER_M_VERSION': cert.get_version(), + 'SSL_SERVER_M_SERIAL': cert.get_serial_number(), +## 'SSL_SERVER_V_START': Validity of server's certificate (start time), +## 'SSL_SERVER_V_END': Validity of server's certificate (end time), + }) + + for prefix, dn in [("I", cert.get_issuer()), + ("S", cert.get_subject())]: + # X509Name objects don't seem to have a way to get the + # complete DN string. Use str() and slice it instead, + # because str(dn) == "" + dnstr = str(dn)[18:-2] + + wsgikey = 'SSL_SERVER_%s_DN' % prefix + ssl_environ[wsgikey] = dnstr + + # The DN should be of the form: /k1=v1/k2=v2, but we must allow + # for any value to contain slashes itself (in a URL). + while dnstr: + pos = dnstr.rfind("=") + dnstr, value = dnstr[:pos], dnstr[pos + 1:] + pos = dnstr.rfind("/") + dnstr, key = dnstr[:pos], dnstr[pos + 1:] + if key and value: + wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key) + ssl_environ[wsgikey] = value + + return ssl_environ + + def makefile(self, sock, mode='r', bufsize=-1): + if SSL and isinstance(sock, SSL.ConnectionType): + timeout = sock.gettimeout() + f = SSL_fileobject(sock, mode, bufsize) + f.ssl_timeout = timeout + return f + else: + return wsgiserver.CP_fileobject(sock, mode, bufsize) + diff --git a/libs/cherrypy/wsgiserver/wsgiserver2.py b/libs/cherrypy/wsgiserver/wsgiserver2.py new file mode 100644 index 0000000..b6bd499 --- /dev/null +++ b/libs/cherrypy/wsgiserver/wsgiserver2.py @@ -0,0 +1,2322 @@ +"""A high-speed, production ready, thread pooled, generic HTTP server. + +Simplest example on how to use this module directly +(without using CherryPy's application machinery):: + + from cherrypy import wsgiserver + + def my_crazy_app(environ, start_response): + status = '200 OK' + response_headers = [('Content-type','text/plain')] + start_response(status, response_headers) + return ['Hello world!'] + + server = wsgiserver.CherryPyWSGIServer( + ('0.0.0.0', 8070), my_crazy_app, + server_name='www.cherrypy.example') + server.start() + +The CherryPy WSGI server can serve as many WSGI applications +as you want in one instance by using a WSGIPathInfoDispatcher:: + + d = WSGIPathInfoDispatcher({'/': my_crazy_app, '/blog': my_blog_app}) + server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', 80), d) + +Want SSL support? Just set server.ssl_adapter to an SSLAdapter instance. + +This won't call the CherryPy engine (application side) at all, only the +HTTP server, which is independent from the rest of CherryPy. Don't +let the name "CherryPyWSGIServer" throw you; the name merely reflects +its origin, not its coupling. + +For those of you wanting to understand internals of this module, here's the +basic call flow. The server's listening thread runs a very tight loop, +sticking incoming connections onto a Queue:: + + server = CherryPyWSGIServer(...) + server.start() + while True: + tick() + # This blocks until a request comes in: + child = socket.accept() + conn = HTTPConnection(child, ...) + server.requests.put(conn) + +Worker threads are kept in a pool and poll the Queue, popping off and then +handling each connection in turn. Each connection can consist of an arbitrary +number of requests and their responses, so we run a nested loop:: + + while True: + conn = server.requests.get() + conn.communicate() + -> while True: + req = HTTPRequest(...) + req.parse_request() + -> # Read the Request-Line, e.g. "GET /page HTTP/1.1" + req.rfile.readline() + read_headers(req.rfile, req.inheaders) + req.respond() + -> response = app(...) + try: + for chunk in response: + if chunk: + req.write(chunk) + finally: + if hasattr(response, "close"): + response.close() + if req.close_connection: + return +""" + +__all__ = ['HTTPRequest', 'HTTPConnection', 'HTTPServer', + 'SizeCheckWrapper', 'KnownLengthRFile', 'ChunkedRFile', + 'CP_fileobject', + 'MaxSizeExceeded', 'NoSSLError', 'FatalSSLAlert', + 'WorkerThread', 'ThreadPool', 'SSLAdapter', + 'CherryPyWSGIServer', + 'Gateway', 'WSGIGateway', 'WSGIGateway_10', 'WSGIGateway_u0', + 'WSGIPathInfoDispatcher', 'get_ssl_adapter_class'] + +import os +try: + import queue +except: + import Queue as queue +import re +import rfc822 +import socket +import sys +if 'win' in sys.platform and not hasattr(socket, 'IPPROTO_IPV6'): + socket.IPPROTO_IPV6 = 41 +try: + import cStringIO as StringIO +except ImportError: + import StringIO +DEFAULT_BUFFER_SIZE = -1 + +_fileobject_uses_str_type = isinstance(socket._fileobject(None)._rbuf, basestring) + +import threading +import time +import traceback +def format_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 + + +from urllib import unquote +from urlparse import urlparse +import warnings + +if sys.version_info >= (3, 0): + bytestr = bytes + unicodestr = str + basestring = (bytes, str) + def ntob(n, encoding='ISO-8859-1'): + """Return the given native string as a byte string in the given encoding.""" + # In Python 3, the native string type is unicode + return n.encode(encoding) +else: + bytestr = str + unicodestr = unicode + basestring = basestring + def ntob(n, encoding='ISO-8859-1'): + """Return the given native string as a byte string in the given encoding.""" + # In Python 2, the native string type is bytes. Assume it's already + # in the given encoding, which for ISO-8859-1 is almost always what + # was intended. + return n + +LF = ntob('\n') +CRLF = ntob('\r\n') +TAB = ntob('\t') +SPACE = ntob(' ') +COLON = ntob(':') +SEMICOLON = ntob(';') +EMPTY = ntob('') +NUMBER_SIGN = ntob('#') +QUESTION_MARK = ntob('?') +ASTERISK = ntob('*') +FORWARD_SLASH = ntob('/') +quoted_slash = re.compile(ntob("(?i)%2F")) + +import errno + +def plat_specific_errors(*errnames): + """Return error numbers for all errors in errnames on this platform. + + The 'errno' module contains different global constants depending on + the specific platform (OS). This function will return the list of + numeric values for a given list of potential names. + """ + errno_names = dir(errno) + nums = [getattr(errno, k) for k in errnames if k in errno_names] + # de-dupe the list + return list(dict.fromkeys(nums).keys()) + +socket_error_eintr = plat_specific_errors("EINTR", "WSAEINTR") + +socket_errors_to_ignore = plat_specific_errors( + "EPIPE", + "EBADF", "WSAEBADF", + "ENOTSOCK", "WSAENOTSOCK", + "ETIMEDOUT", "WSAETIMEDOUT", + "ECONNREFUSED", "WSAECONNREFUSED", + "ECONNRESET", "WSAECONNRESET", + "ECONNABORTED", "WSAECONNABORTED", + "ENETRESET", "WSAENETRESET", + "EHOSTDOWN", "EHOSTUNREACH", + ) +socket_errors_to_ignore.append("timed out") +socket_errors_to_ignore.append("The read operation timed out") + +socket_errors_nonblocking = plat_specific_errors( + 'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK') + +comma_separated_headers = [ntob(h) for h in + ['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']] + + +import logging +if not hasattr(logging, 'statistics'): logging.statistics = {} + + +def read_headers(rfile, hdict=None): + """Read headers from the given stream into the given header dict. + + If hdict is None, a new header dict is created. Returns the populated + header dict. + + Headers which are repeated are folded together using a comma if their + specification so dictates. + + This function raises ValueError when the read bytes violate the HTTP spec. + You should probably return "400 Bad Request" if this happens. + """ + if hdict is None: + hdict = {} + + while True: + line = rfile.readline() + if not line: + # No more data--illegal end of headers + raise ValueError("Illegal end of headers.") + + if line == CRLF: + # Normal end of headers + break + if not line.endswith(CRLF): + raise ValueError("HTTP requires CRLF terminators") + + if line[0] in (SPACE, TAB): + # It's a continuation line. + v = line.strip() + else: + try: + k, v = line.split(COLON, 1) + except ValueError: + raise ValueError("Illegal header line.") + # TODO: what about TE and WWW-Authenticate? + k = k.strip().title() + v = v.strip() + hname = k + + if k in comma_separated_headers: + existing = hdict.get(hname) + if existing: + v = ", ".join((existing, v)) + hdict[hname] = v + + return hdict + + +class MaxSizeExceeded(Exception): + pass + +class SizeCheckWrapper(object): + """Wraps a file-like object, raising MaxSizeExceeded if too large.""" + + def __init__(self, rfile, maxlen): + self.rfile = rfile + self.maxlen = maxlen + self.bytes_read = 0 + + def _check_length(self): + if self.maxlen and self.bytes_read > self.maxlen: + raise MaxSizeExceeded() + + def read(self, size=None): + data = self.rfile.read(size) + self.bytes_read += len(data) + self._check_length() + return data + + def readline(self, size=None): + if size is not None: + data = self.rfile.readline(size) + self.bytes_read += len(data) + self._check_length() + return data + + # User didn't specify a size ... + # We read the line in chunks to make sure it's not a 100MB line ! + res = [] + while True: + data = self.rfile.readline(256) + self.bytes_read += len(data) + self._check_length() + res.append(data) + # See http://www.cherrypy.org/ticket/421 + if len(data) < 256 or data[-1:] == "\n": + return EMPTY.join(res) + + def readlines(self, sizehint=0): + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline() + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline() + return lines + + def close(self): + self.rfile.close() + + def __iter__(self): + return self + + def __next__(self): + data = next(self.rfile) + self.bytes_read += len(data) + self._check_length() + return data + + def next(self): + data = self.rfile.next() + self.bytes_read += len(data) + self._check_length() + return data + + +class KnownLengthRFile(object): + """Wraps a file-like object, returning an empty string when exhausted.""" + + def __init__(self, rfile, content_length): + self.rfile = rfile + self.remaining = content_length + + def read(self, size=None): + if self.remaining == 0: + return '' + if size is None: + size = self.remaining + else: + size = min(size, self.remaining) + + data = self.rfile.read(size) + self.remaining -= len(data) + return data + + def readline(self, size=None): + if self.remaining == 0: + return '' + if size is None: + size = self.remaining + else: + size = min(size, self.remaining) + + data = self.rfile.readline(size) + self.remaining -= len(data) + return data + + def readlines(self, sizehint=0): + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline(sizehint) + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + return lines + + def close(self): + self.rfile.close() + + def __iter__(self): + return self + + def __next__(self): + data = next(self.rfile) + self.remaining -= len(data) + return data + + +class ChunkedRFile(object): + """Wraps a file-like object, returning an empty string when exhausted. + + This class is intended to provide a conforming wsgi.input value for + request entities that have been encoded with the 'chunked' transfer + encoding. + """ + + def __init__(self, rfile, maxlen, bufsize=8192): + self.rfile = rfile + self.maxlen = maxlen + self.bytes_read = 0 + self.buffer = EMPTY + self.bufsize = bufsize + self.closed = False + + def _fetch(self): + if self.closed: + return + + line = self.rfile.readline() + self.bytes_read += len(line) + + if self.maxlen and self.bytes_read > self.maxlen: + raise MaxSizeExceeded("Request Entity Too Large", self.maxlen) + + line = line.strip().split(SEMICOLON, 1) + + try: + chunk_size = line.pop(0) + chunk_size = int(chunk_size, 16) + except ValueError: + raise ValueError("Bad chunked transfer size: " + repr(chunk_size)) + + if chunk_size <= 0: + self.closed = True + return + +## if line: chunk_extension = line[0] + + if self.maxlen and self.bytes_read + chunk_size > self.maxlen: + raise IOError("Request Entity Too Large") + + chunk = self.rfile.read(chunk_size) + self.bytes_read += len(chunk) + self.buffer += chunk + + crlf = self.rfile.read(2) + if crlf != CRLF: + raise ValueError( + "Bad chunked transfer coding (expected '\\r\\n', " + "got " + repr(crlf) + ")") + + def read(self, size=None): + data = EMPTY + while True: + if size and len(data) >= size: + return data + + if not self.buffer: + self._fetch() + if not self.buffer: + # EOF + return data + + if size: + remaining = size - len(data) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + data += self.buffer + + def readline(self, size=None): + data = EMPTY + while True: + if size and len(data) >= size: + return data + + if not self.buffer: + self._fetch() + if not self.buffer: + # EOF + return data + + newline_pos = self.buffer.find(LF) + if size: + if newline_pos == -1: + remaining = size - len(data) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + remaining = min(size - len(data), newline_pos) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + if newline_pos == -1: + data += self.buffer + else: + data += self.buffer[:newline_pos] + self.buffer = self.buffer[newline_pos:] + + def readlines(self, sizehint=0): + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline(sizehint) + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + return lines + + def read_trailer_lines(self): + if not self.closed: + raise ValueError( + "Cannot read trailers until the request body has been read.") + + while True: + line = self.rfile.readline() + if not line: + # No more data--illegal end of headers + raise ValueError("Illegal end of headers.") + + self.bytes_read += len(line) + if self.maxlen and self.bytes_read > self.maxlen: + raise IOError("Request Entity Too Large") + + if line == CRLF: + # Normal end of headers + break + if not line.endswith(CRLF): + raise ValueError("HTTP requires CRLF terminators") + + yield line + + def close(self): + self.rfile.close() + + def __iter__(self): + # Shamelessly stolen from StringIO + total = 0 + line = self.readline(sizehint) + while line: + yield line + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + + +class HTTPRequest(object): + """An HTTP Request (and response). + + A single HTTP connection may consist of multiple request/response pairs. + """ + + server = None + """The HTTPServer object which is receiving this request.""" + + conn = None + """The HTTPConnection object on which this request connected.""" + + inheaders = {} + """A dict of request headers.""" + + outheaders = [] + """A list of header tuples to write in the response.""" + + ready = False + """When True, the request has been parsed and is ready to begin generating + the response. When False, signals the calling Connection that the response + should not be generated and the connection should close.""" + + close_connection = False + """Signals the calling Connection that the request should close. This does + not imply an error! The client and/or server may each request that the + connection be closed.""" + + chunked_write = False + """If True, output will be encoded with the "chunked" transfer-coding. + + This value is set automatically inside send_headers.""" + + def __init__(self, server, conn): + self.server= server + self.conn = conn + + self.ready = False + self.started_request = False + self.scheme = ntob("http") + if self.server.ssl_adapter is not None: + self.scheme = ntob("https") + # Use the lowest-common protocol in case read_request_line errors. + self.response_protocol = 'HTTP/1.0' + self.inheaders = {} + + self.status = "" + self.outheaders = [] + self.sent_headers = False + self.close_connection = self.__class__.close_connection + self.chunked_read = False + self.chunked_write = self.__class__.chunked_write + + def parse_request(self): + """Parse the next HTTP request start-line and message-headers.""" + self.rfile = SizeCheckWrapper(self.conn.rfile, + self.server.max_request_header_size) + try: + success = self.read_request_line() + except MaxSizeExceeded: + self.simple_response("414 Request-URI Too Long", + "The Request-URI sent with the request exceeds the maximum " + "allowed bytes.") + return + else: + if not success: + return + + try: + success = self.read_request_headers() + except MaxSizeExceeded: + self.simple_response("413 Request Entity Too Large", + "The headers sent with the request exceed the maximum " + "allowed bytes.") + return + else: + if not success: + return + + self.ready = True + + def read_request_line(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() + + # Set started_request to True so communicate() knows to send 408 + # from here on out. + self.started_request = True + if not request_line: + return False + + if request_line == CRLF: + # RFC 2616 sec 4.1: "...if the server is reading the protocol + # stream at the beginning of a message and receives a CRLF + # first, it should ignore the CRLF." + # But only ignore one leading line! else we enable a DoS. + request_line = self.rfile.readline() + if not request_line: + return False + + if not request_line.endswith(CRLF): + self.simple_response("400 Bad Request", "HTTP requires CRLF terminators") + return False + + try: + method, uri, req_protocol = request_line.strip().split(SPACE, 2) + rp = int(req_protocol[5]), int(req_protocol[7]) + except (ValueError, IndexError): + self.simple_response("400 Bad Request", "Malformed Request-Line") + return False + + self.uri = uri + self.method = method + + # uri may be an abs_path (including "http://host.domain.tld"); + scheme, authority, path = self.parse_request_uri(uri) + if NUMBER_SIGN in path: + self.simple_response("400 Bad Request", + "Illegal #fragment in Request-URI.") + return False + + if scheme: + self.scheme = scheme + + qs = EMPTY + if QUESTION_MARK in path: + path, qs = path.split(QUESTION_MARK, 1) + + # 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.2 + # Therefore, "/this%2Fpath" becomes "/this%2Fpath", not "/this/path". + try: + atoms = [unquote(x) for x in quoted_slash.split(path)] + except ValueError: + ex = sys.exc_info()[1] + self.simple_response("400 Bad Request", ex.args[0]) + return False + path = "%2F".join(atoms) + self.path = path + + # Note that, like wsgiref and most other HTTP servers, + # we "% HEX HEX"-unquote the path but not the query string. + self.qs = 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. + sp = int(self.server.protocol[5]), int(self.server.protocol[7]) + + if sp[0] != rp[0]: + self.simple_response("505 HTTP Version Not Supported") + return False + + self.request_protocol = req_protocol + self.response_protocol = "HTTP/%s.%s" % min(rp, sp) + + return True + + def read_request_headers(self): + """Read self.rfile into self.inheaders. Return success.""" + + # then all the http headers + try: + read_headers(self.rfile, self.inheaders) + except ValueError: + ex = sys.exc_info()[1] + self.simple_response("400 Bad Request", ex.args[0]) + return False + + mrbs = self.server.max_request_body_size + if mrbs and int(self.inheaders.get("Content-Length", 0)) > mrbs: + self.simple_response("413 Request Entity Too Large", + "The entity sent with the request exceeds the maximum " + "allowed bytes.") + return False + + # Persistent connection support + if self.response_protocol == "HTTP/1.1": + # Both server and client are HTTP/1.1 + if self.inheaders.get("Connection", "") == "close": + self.close_connection = True + else: + # Either the server or client (or both) are HTTP/1.0 + if self.inheaders.get("Connection", "") != "Keep-Alive": + self.close_connection = True + + # Transfer-Encoding support + te = None + if self.response_protocol == "HTTP/1.1": + te = self.inheaders.get("Transfer-Encoding") + if te: + te = [x.strip().lower() for x in te.split(",") if x.strip()] + + self.chunked_read = False + + if te: + for enc in te: + if enc == "chunked": + self.chunked_read = True + else: + # Note that, even if we see "chunked", we must reject + # if there is an extension we don't recognize. + self.simple_response("501 Unimplemented") + self.close_connection = True + return False + + # 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. + if self.inheaders.get("Expect", "") == "100-continue": + # Don't use simple_response here, because it emits headers + # we don't want. See http://www.cherrypy.org/ticket/951 + msg = self.server.protocol + " 100 Continue\r\n\r\n" + try: + self.conn.wfile.sendall(msg) + except socket.error: + x = sys.exc_info()[1] + if x.args[0] not in socket_errors_to_ignore: + raise + return True + + def parse_request_uri(self, uri): + """Parse a Request-URI into (scheme, authority, path). + + Note that Request-URI's must be one of:: + + Request-URI = "*" | absoluteURI | abs_path | authority + + Therefore, a Request-URI which starts with a double forward-slash + cannot be a "net_path":: + + net_path = "//" authority [ abs_path ] + + Instead, it must be interpreted as an "abs_path" with an empty first + path segment:: + + abs_path = "/" path_segments + path_segments = segment *( "/" segment ) + segment = *pchar *( ";" param ) + param = *pchar + """ + if uri == ASTERISK: + return None, None, uri + + i = uri.find('://') + if i > 0 and QUESTION_MARK not in uri[:i]: + # An absoluteURI. + # If there's a scheme (and it must be http or https), then: + # http_URL = "http:" "//" host [ ":" port ] [ abs_path [ "?" query ]] + scheme, remainder = uri[:i].lower(), uri[i + 3:] + authority, path = remainder.split(FORWARD_SLASH, 1) + path = FORWARD_SLASH + path + return scheme, authority, path + + if uri.startswith(FORWARD_SLASH): + # An abs_path. + return None, None, uri + else: + # An authority. + return None, uri, None + + def respond(self): + """Call the gateway and write its iterable output.""" + mrbs = self.server.max_request_body_size + if self.chunked_read: + self.rfile = ChunkedRFile(self.conn.rfile, mrbs) + else: + cl = int(self.inheaders.get("Content-Length", 0)) + if mrbs and mrbs < cl: + if not self.sent_headers: + self.simple_response("413 Request Entity Too Large", + "The entity sent with the request exceeds the maximum " + "allowed bytes.") + return + self.rfile = KnownLengthRFile(self.conn.rfile, cl) + + self.server.gateway(self).respond() + + if (self.ready and not self.sent_headers): + self.sent_headers = True + self.send_headers() + if self.chunked_write: + self.conn.wfile.sendall("0\r\n\r\n") + + def simple_response(self, status, msg=""): + """Write a simple response back to the client.""" + status = str(status) + buf = [self.server.protocol + SPACE + + status + CRLF, + "Content-Length: %s\r\n" % len(msg), + "Content-Type: text/plain\r\n"] + + if status[:3] in ("413", "414"): + # Request Entity Too Large / Request-URI Too Long + self.close_connection = True + if self.response_protocol == 'HTTP/1.1': + # This will not be true for 414, since read_request_line + # usually raises 414 before reading the whole line, and we + # therefore cannot know the proper response_protocol. + buf.append("Connection: close\r\n") + else: + # HTTP/1.0 had no 413/414 status nor Connection header. + # Emit 400 instead and trust the message body is enough. + status = "400 Bad Request" + + buf.append(CRLF) + if msg: + if isinstance(msg, unicodestr): + msg = msg.encode("ISO-8859-1") + buf.append(msg) + + try: + self.conn.wfile.sendall("".join(buf)) + except socket.error: + x = sys.exc_info()[1] + if x.args[0] not in socket_errors_to_ignore: + raise + + def write(self, chunk): + """Write unbuffered data to the client.""" + if self.chunked_write and chunk: + buf = [hex(len(chunk))[2:], CRLF, chunk, CRLF] + self.conn.wfile.sendall(EMPTY.join(buf)) + else: + self.conn.wfile.sendall(chunk) + + def send_headers(self): + """Assert, process, and send the HTTP response message-headers. + + You must set self.status, and self.outheaders before calling this. + """ + hkeys = [key.lower() for key, value in self.outheaders] + status = int(self.status[:3]) + + if status == 413: + # Request Entity Too Large. Close conn to avoid garbage. + self.close_connection = True + elif "content-length" not in hkeys: + # "All 1xx (informational), 204 (no content), + # and 304 (not modified) responses MUST NOT + # include a message-body." So no point chunking. + if status < 200 or status in (204, 205, 304): + pass + else: + if (self.response_protocol == 'HTTP/1.1' + and self.method != 'HEAD'): + # Use the chunked transfer-coding + self.chunked_write = True + self.outheaders.append(("Transfer-Encoding", "chunked")) + else: + # Closing the conn is the only way to determine len. + self.close_connection = True + + if "connection" not in hkeys: + if self.response_protocol == 'HTTP/1.1': + # Both server and client are HTTP/1.1 or better + if self.close_connection: + self.outheaders.append(("Connection", "close")) + else: + # Server and/or client are HTTP/1.0 + if not self.close_connection: + self.outheaders.append(("Connection", "Keep-Alive")) + + if (not self.close_connection) and (not self.chunked_read): + # Read any remaining request body data on the socket. + # "If an origin server receives a request that does not include an + # Expect request-header field with the "100-continue" expectation, + # the request includes a request body, and the server responds + # with a final status code before reading the entire request body + # from the transport connection, then the server SHOULD NOT close + # the transport connection until it has read the entire request, + # or until the client closes the connection. Otherwise, the client + # might not reliably receive the response message. However, this + # requirement is not be construed as preventing a server from + # defending itself against denial-of-service attacks, or from + # badly broken client implementations." + remaining = getattr(self.rfile, 'remaining', 0) + if remaining > 0: + self.rfile.read(remaining) + + if "date" not in hkeys: + self.outheaders.append(("Date", rfc822.formatdate())) + + if "server" not in hkeys: + self.outheaders.append(("Server", self.server.server_name)) + + buf = [self.server.protocol + SPACE + self.status + CRLF] + for k, v in self.outheaders: + buf.append(k + COLON + SPACE + v + CRLF) + buf.append(CRLF) + self.conn.wfile.sendall(EMPTY.join(buf)) + + +class NoSSLError(Exception): + """Exception raised when a client speaks HTTP to an HTTPS socket.""" + pass + + +class FatalSSLAlert(Exception): + """Exception raised when the SSL implementation signals a fatal alert.""" + pass + + +class CP_fileobject(socket._fileobject): + """Faux file object attached to a socket object.""" + + def __init__(self, *args, **kwargs): + self.bytes_read = 0 + self.bytes_written = 0 + socket._fileobject.__init__(self, *args, **kwargs) + + def sendall(self, data): + """Sendall for non-blocking sockets.""" + while data: + try: + bytes_sent = self.send(data) + data = data[bytes_sent:] + except socket.error, e: + if e.args[0] not in socket_errors_nonblocking: + raise + + def send(self, data): + bytes_sent = self._sock.send(data) + self.bytes_written += bytes_sent + return bytes_sent + + def flush(self): + if self._wbuf: + buffer = "".join(self._wbuf) + self._wbuf = [] + self.sendall(buffer) + + def recv(self, size): + while True: + try: + data = self._sock.recv(size) + self.bytes_read += len(data) + return data + except socket.error, e: + if (e.args[0] not in socket_errors_nonblocking + and e.args[0] not in socket_error_eintr): + raise + + if not _fileobject_uses_str_type: + def read(self, size=-1): + # Use max, disallow tiny reads in a loop as they are very inefficient. + # We never leave read() with any leftover data from a new recv() call + # in our internal buffer. + rbufsize = max(self._rbufsize, self.default_bufsize) + # Our use of StringIO rather than lists of string objects returned by + # recv() minimizes memory usage and fragmentation that occurs when + # rbufsize is large compared to the typical return value of recv(). + buf = self._rbuf + buf.seek(0, 2) # seek end + if size < 0: + # Read until EOF + self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. + while True: + data = self.recv(rbufsize) + if not data: + break + buf.write(data) + return buf.getvalue() + else: + # Read until size bytes or EOF seen, whichever comes first + buf_len = buf.tell() + if buf_len >= size: + # Already have size bytes in our buffer? Extract and return. + buf.seek(0) + rv = buf.read(size) + self._rbuf = StringIO.StringIO() + self._rbuf.write(buf.read()) + return rv + + self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. + while True: + left = size - buf_len + # recv() will malloc the amount of memory given as its + # parameter even though it often returns much less data + # than that. The returned data string is short lived + # as we copy it into a StringIO and free it. This avoids + # fragmentation issues on many platforms. + data = self.recv(left) + if not data: + break + n = len(data) + if n == size and not buf_len: + # Shortcut. Avoid buffer data copies when: + # - We have no data in our buffer. + # AND + # - Our call to recv returned exactly the + # number of bytes we were asked to read. + return data + if n == left: + buf.write(data) + del data # explicit free + break + assert n <= left, "recv(%d) returned %d bytes" % (left, n) + buf.write(data) + buf_len += n + del data # explicit free + #assert buf_len == buf.tell() + return buf.getvalue() + + def readline(self, size=-1): + buf = self._rbuf + buf.seek(0, 2) # seek end + if buf.tell() > 0: + # check if we already have it in our buffer + buf.seek(0) + bline = buf.readline(size) + if bline.endswith('\n') or len(bline) == size: + self._rbuf = StringIO.StringIO() + self._rbuf.write(buf.read()) + return bline + del bline + if size < 0: + # Read until \n or EOF, whichever comes first + if self._rbufsize <= 1: + # Speed up unbuffered case + buf.seek(0) + buffers = [buf.read()] + self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. + data = None + recv = self.recv + while data != "\n": + data = recv(1) + if not data: + break + buffers.append(data) + return "".join(buffers) + + buf.seek(0, 2) # seek end + self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. + while True: + data = self.recv(self._rbufsize) + if not data: + break + nl = data.find('\n') + if nl >= 0: + nl += 1 + buf.write(data[:nl]) + self._rbuf.write(data[nl:]) + del data + break + buf.write(data) + return buf.getvalue() + else: + # Read until size bytes or \n or EOF seen, whichever comes first + buf.seek(0, 2) # seek end + buf_len = buf.tell() + if buf_len >= size: + buf.seek(0) + rv = buf.read(size) + self._rbuf = StringIO.StringIO() + self._rbuf.write(buf.read()) + return rv + self._rbuf = StringIO.StringIO() # reset _rbuf. we consume it via buf. + while True: + data = self.recv(self._rbufsize) + if not data: + break + left = size - buf_len + # did we just receive a newline? + nl = data.find('\n', 0, left) + if nl >= 0: + nl += 1 + # save the excess data to _rbuf + self._rbuf.write(data[nl:]) + if buf_len: + buf.write(data[:nl]) + break + else: + # Shortcut. Avoid data copy through buf when returning + # a substring of our first recv(). + return data[:nl] + n = len(data) + if n == size and not buf_len: + # Shortcut. Avoid data copy through buf when + # returning exactly all of our first recv(). + return data + if n >= left: + buf.write(data[:left]) + self._rbuf.write(data[left:]) + break + buf.write(data) + buf_len += n + #assert buf_len == buf.tell() + return buf.getvalue() + else: + def read(self, size=-1): + if size < 0: + # Read until EOF + buffers = [self._rbuf] + self._rbuf = "" + if self._rbufsize <= 1: + recv_size = self.default_bufsize + else: + recv_size = self._rbufsize + + while True: + data = self.recv(recv_size) + if not data: + break + buffers.append(data) + return "".join(buffers) + else: + # Read until size bytes or EOF seen, whichever comes first + data = self._rbuf + buf_len = len(data) + if buf_len >= size: + self._rbuf = data[size:] + return data[:size] + buffers = [] + if data: + buffers.append(data) + self._rbuf = "" + while True: + left = size - buf_len + recv_size = max(self._rbufsize, left) + data = self.recv(recv_size) + if not data: + break + buffers.append(data) + n = len(data) + if n >= left: + self._rbuf = data[left:] + buffers[-1] = data[:left] + break + buf_len += n + return "".join(buffers) + + def readline(self, size=-1): + data = self._rbuf + if size < 0: + # Read until \n or EOF, whichever comes first + if self._rbufsize <= 1: + # Speed up unbuffered case + assert data == "" + buffers = [] + while data != "\n": + data = self.recv(1) + if not data: + break + buffers.append(data) + return "".join(buffers) + nl = data.find('\n') + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + return data[:nl] + buffers = [] + if data: + buffers.append(data) + self._rbuf = "" + while True: + data = self.recv(self._rbufsize) + if not data: + break + buffers.append(data) + nl = data.find('\n') + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + buffers[-1] = data[:nl] + break + return "".join(buffers) + else: + # Read until size bytes or \n or EOF seen, whichever comes first + nl = data.find('\n', 0, size) + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + return data[:nl] + buf_len = len(data) + if buf_len >= size: + self._rbuf = data[size:] + return data[:size] + buffers = [] + if data: + buffers.append(data) + self._rbuf = "" + while True: + data = self.recv(self._rbufsize) + if not data: + break + buffers.append(data) + left = size - buf_len + nl = data.find('\n', 0, left) + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + buffers[-1] = data[:nl] + break + n = len(data) + if n >= left: + self._rbuf = data[left:] + buffers[-1] = data[:left] + break + buf_len += n + return "".join(buffers) + + +class HTTPConnection(object): + """An HTTP connection (active socket). + + server: the Server object which received this connection. + socket: the raw socket object (usually TCP) for this connection. + makefile: a fileobject class for reading from the socket. + """ + + remote_addr = None + remote_port = None + ssl_env = None + rbufsize = DEFAULT_BUFFER_SIZE + wbufsize = DEFAULT_BUFFER_SIZE + RequestHandlerClass = HTTPRequest + + def __init__(self, server, sock, makefile=CP_fileobject): + self.server = server + self.socket = sock + self.rfile = makefile(sock, "rb", self.rbufsize) + self.wfile = makefile(sock, "wb", self.wbufsize) + self.requests_seen = 0 + + def communicate(self): + """Read each request and respond appropriately.""" + request_seen = False + try: + while True: + # (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 = None + req = self.RequestHandlerClass(self.server, self) + + # This order of operations should guarantee correct pipelining. + req.parse_request() + if self.server.stats['Enabled']: + self.requests_seen += 1 + if not req.ready: + # Something went wrong in the parsing (and the server has + # probably already made a simple_response). Return and + # let the conn close. + return + + request_seen = True + req.respond() + if req.close_connection: + return + except socket.error: + e = sys.exc_info()[1] + errnum = e.args[0] + # sadly SSL sockets return a different (longer) time out string + if errnum == 'timed out' or errnum == 'The read operation timed out': + # Don't error if we're between requests; only error + # if 1) no request has been started at all, or 2) we're + # in the middle of a request. + # See http://www.cherrypy.org/ticket/853 + if (not request_seen) or (req and req.started_request): + # Don't bother writing the 408 if the response + # has already started being written. + if req and not req.sent_headers: + try: + req.simple_response("408 Request Timeout") + except FatalSSLAlert: + # Close the connection. + return + elif errnum not in socket_errors_to_ignore: + self.server.error_log("socket.error %s" % repr(errnum), + level=logging.WARNING, traceback=True) + if req and not req.sent_headers: + try: + req.simple_response("500 Internal Server Error") + except FatalSSLAlert: + # Close the connection. + return + return + except (KeyboardInterrupt, SystemExit): + raise + except FatalSSLAlert: + # Close the connection. + return + except NoSSLError: + if req and not req.sent_headers: + # Unwrap our wfile + self.wfile = CP_fileobject(self.socket._sock, "wb", self.wbufsize) + req.simple_response("400 Bad Request", + "The client sent a plain HTTP request, but " + "this server only speaks HTTPS on this port.") + self.linger = True + except Exception: + e = sys.exc_info()[1] + self.server.error_log(repr(e), level=logging.ERROR, traceback=True) + if req and not req.sent_headers: + try: + req.simple_response("500 Internal Server Error") + except FatalSSLAlert: + # Close the connection. + return + + linger = False + + def close(self): + """Close the socket underlying this connection.""" + self.rfile.close() + + if not self.linger: + # Python's socket module does NOT call close on the kernel socket + # when you call socket.close(). We do so manually here because we + # want this server to send a FIN TCP segment immediately. Note this + # must be called *before* calling socket.close(), because the latter + # drops its reference to the kernel socket. + if hasattr(self.socket, '_sock'): + self.socket._sock.close() + self.socket.close() + else: + # On the other hand, sometimes we want to hang around for a bit + # to make sure the client has a chance to read our entire + # response. Skipping the close() calls here delays the FIN + # packet until the socket object is garbage-collected later. + # Someday, perhaps, we'll do the full lingering_close that + # Apache does, but not today. + pass + + +class TrueyZero(object): + """An object which equals and does math like the integer '0' but evals True.""" + def __add__(self, other): + return other + def __radd__(self, other): + return other +trueyzero = TrueyZero() + + +_SHUTDOWNREQUEST = None + +class WorkerThread(threading.Thread): + """Thread which continuously polls a Queue for Connection objects. + + Due to the timing issues of polling a Queue, a WorkerThread does not + check its own 'ready' flag after it has started. To stop the thread, + it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue + (one for each running WorkerThread). + """ + + conn = None + """The current connection pulled off the Queue, or None.""" + + server = None + """The HTTP Server which spawned this thread, and which owns the + Queue and is placing active connections into it.""" + + ready = False + """A simple flag for the calling server to know when this thread + has begun polling the Queue.""" + + + def __init__(self, server): + self.ready = False + self.server = server + + self.requests_seen = 0 + self.bytes_read = 0 + self.bytes_written = 0 + self.start_time = None + self.work_time = 0 + self.stats = { + 'Requests': lambda s: self.requests_seen + ((self.start_time is None) and trueyzero or self.conn.requests_seen), + 'Bytes Read': lambda s: self.bytes_read + ((self.start_time is None) and trueyzero or self.conn.rfile.bytes_read), + 'Bytes Written': lambda s: self.bytes_written + ((self.start_time is None) and trueyzero or self.conn.wfile.bytes_written), + 'Work Time': lambda s: self.work_time + ((self.start_time is None) and trueyzero or time.time() - self.start_time), + 'Read Throughput': lambda s: s['Bytes Read'](s) / (s['Work Time'](s) or 1e-6), + 'Write Throughput': lambda s: s['Bytes Written'](s) / (s['Work Time'](s) or 1e-6), + } + threading.Thread.__init__(self) + + def run(self): + self.server.stats['Worker Threads'][self.getName()] = self.stats + try: + self.ready = True + while True: + conn = self.server.requests.get() + if conn is _SHUTDOWNREQUEST: + return + + self.conn = conn + if self.server.stats['Enabled']: + self.start_time = time.time() + try: + conn.communicate() + finally: + conn.close() + if self.server.stats['Enabled']: + self.requests_seen += self.conn.requests_seen + self.bytes_read += self.conn.rfile.bytes_read + self.bytes_written += self.conn.wfile.bytes_written + self.work_time += time.time() - self.start_time + self.start_time = None + self.conn = None + except (KeyboardInterrupt, SystemExit): + exc = sys.exc_info()[1] + self.server.interrupt = exc + + +class ThreadPool(object): + """A Request Queue for an HTTPServer which pools threads. + + ThreadPool objects must provide min, get(), put(obj), start() + and stop(timeout) attributes. + """ + + def __init__(self, server, min=10, max=-1): + self.server = server + self.min = min + self.max = max + self._threads = [] + self._queue = queue.Queue() + self.get = self._queue.get + + def start(self): + """Start the pool of threads.""" + for i in range(self.min): + self._threads.append(WorkerThread(self.server)) + for worker in self._threads: + worker.setName("CP Server " + worker.getName()) + worker.start() + for worker in self._threads: + while not worker.ready: + time.sleep(.1) + + def _get_idle(self): + """Number of worker threads which are idle. Read-only.""" + return len([t for t in self._threads if t.conn is None]) + idle = property(_get_idle, doc=_get_idle.__doc__) + + def put(self, obj): + self._queue.put(obj) + if obj is _SHUTDOWNREQUEST: + return + + def grow(self, amount): + """Spawn new worker threads (not above self.max).""" + for i in range(amount): + if self.max > 0 and len(self._threads) >= self.max: + break + worker = WorkerThread(self.server) + worker.setName("CP Server " + worker.getName()) + self._threads.append(worker) + worker.start() + + def shrink(self, amount): + """Kill off worker threads (not below self.min).""" + # Grow/shrink the pool if necessary. + # Remove any dead threads from our list + for t in self._threads: + if not t.isAlive(): + self._threads.remove(t) + amount -= 1 + + if amount > 0: + for i in range(min(amount, len(self._threads) - self.min)): + # Put a number of shutdown requests on the queue equal + # to 'amount'. Once each of those is processed by a worker, + # that worker will terminate and be culled from our list + # in self.put. + self._queue.put(_SHUTDOWNREQUEST) + + def stop(self, timeout=5): + # Must shut down threads here so the code that calls + # this method can know when all threads are stopped. + for worker in self._threads: + self._queue.put(_SHUTDOWNREQUEST) + + # Don't join currentThread (when stop is called inside a request). + current = threading.currentThread() + if timeout and timeout >= 0: + endtime = time.time() + timeout + while self._threads: + worker = self._threads.pop() + if worker is not current and worker.isAlive(): + try: + if timeout is None or timeout < 0: + worker.join() + else: + remaining_time = endtime - time.time() + if remaining_time > 0: + worker.join(remaining_time) + if worker.isAlive(): + # We exhausted the timeout. + # Forcibly shut down the socket. + c = worker.conn + if c and not c.rfile.closed: + try: + c.socket.shutdown(socket.SHUT_RD) + except TypeError: + # pyOpenSSL sockets don't take an arg + c.socket.shutdown() + worker.join() + except (AssertionError, + # Ignore repeated Ctrl-C. + # See http://www.cherrypy.org/ticket/691. + KeyboardInterrupt): + pass + + def _get_qsize(self): + return self._queue.qsize() + qsize = property(_get_qsize) + + + +try: + import fcntl +except ImportError: + try: + from ctypes import windll, WinError + except ImportError: + def prevent_socket_inheritance(sock): + """Dummy function, since neither fcntl nor ctypes are available.""" + pass + else: + def prevent_socket_inheritance(sock): + """Mark the given socket fd as non-inheritable (Windows).""" + if not windll.kernel32.SetHandleInformation(sock.fileno(), 1, 0): + raise WinError() +else: + def prevent_socket_inheritance(sock): + """Mark the given socket fd as non-inheritable (POSIX).""" + fd = sock.fileno() + old_flags = fcntl.fcntl(fd, fcntl.F_GETFD) + fcntl.fcntl(fd, fcntl.F_SETFD, old_flags | fcntl.FD_CLOEXEC) + + +class SSLAdapter(object): + """Base class for SSL driver library adapters. + + Required methods: + + * ``wrap(sock) -> (wrapped socket, ssl environ dict)`` + * ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) -> socket file object`` + """ + + def __init__(self, certificate, private_key, certificate_chain=None): + self.certificate = certificate + self.private_key = private_key + self.certificate_chain = certificate_chain + + def wrap(self, sock): + raise NotImplemented + + def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE): + raise NotImplemented + + +class HTTPServer(object): + """An HTTP server.""" + + _bind_addr = "127.0.0.1" + _interrupt = None + + gateway = None + """A Gateway instance.""" + + minthreads = None + """The minimum number of worker threads to create (default 10).""" + + maxthreads = None + """The maximum number of worker threads to create (default -1 = no limit).""" + + server_name = None + """The name of the server; defaults to socket.gethostname().""" + + protocol = "HTTP/1.1" + """The version string to write in the Status-Line of all HTTP responses. + + For example, "HTTP/1.1" is the default. This also limits the supported + features used in the response.""" + + request_queue_size = 5 + """The 'backlog' arg to socket.listen(); max queued connections (default 5).""" + + shutdown_timeout = 5 + """The total time, in seconds, to wait for worker threads to cleanly exit.""" + + timeout = 10 + """The timeout in seconds for accepted connections (default 10).""" + + version = "CherryPy/3.2.2" + """A version string for the HTTPServer.""" + + software = None + """The value to set for the SERVER_SOFTWARE entry in the WSGI environ. + + If None, this defaults to ``'%s Server' % self.version``.""" + + ready = False + """An internal flag which marks whether the socket is accepting connections.""" + + max_request_header_size = 0 + """The maximum size, in bytes, for request headers, or 0 for no limit.""" + + max_request_body_size = 0 + """The maximum size, in bytes, for request bodies, or 0 for no limit.""" + + nodelay = True + """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" + + ConnectionClass = HTTPConnection + """The class to use for handling HTTP connections.""" + + ssl_adapter = None + """An instance of SSLAdapter (or a subclass). + + You must have the corresponding SSL driver library installed.""" + + def __init__(self, bind_addr, gateway, minthreads=10, maxthreads=-1, + server_name=None): + self.bind_addr = bind_addr + self.gateway = gateway + + self.requests = ThreadPool(self, min=minthreads or 1, max=maxthreads) + + if not server_name: + server_name = socket.gethostname() + self.server_name = server_name + self.clear_stats() + + def clear_stats(self): + self._start_time = None + self._run_time = 0 + self.stats = { + 'Enabled': False, + 'Bind Address': lambda s: repr(self.bind_addr), + 'Run time': lambda s: (not s['Enabled']) and -1 or self.runtime(), + 'Accepts': 0, + 'Accepts/sec': lambda s: s['Accepts'] / self.runtime(), + 'Queue': lambda s: getattr(self.requests, "qsize", None), + 'Threads': lambda s: len(getattr(self.requests, "_threads", [])), + 'Threads Idle': lambda s: getattr(self.requests, "idle", None), + 'Socket Errors': 0, + 'Requests': lambda s: (not s['Enabled']) and -1 or sum([w['Requests'](w) for w + in s['Worker Threads'].values()], 0), + 'Bytes Read': lambda s: (not s['Enabled']) and -1 or sum([w['Bytes Read'](w) for w + in s['Worker Threads'].values()], 0), + 'Bytes Written': lambda s: (not s['Enabled']) and -1 or sum([w['Bytes Written'](w) for w + in s['Worker Threads'].values()], 0), + 'Work Time': lambda s: (not s['Enabled']) and -1 or sum([w['Work Time'](w) for w + in s['Worker Threads'].values()], 0), + 'Read Throughput': lambda s: (not s['Enabled']) and -1 or sum( + [w['Bytes Read'](w) / (w['Work Time'](w) or 1e-6) + for w in s['Worker Threads'].values()], 0), + 'Write Throughput': lambda s: (not s['Enabled']) and -1 or sum( + [w['Bytes Written'](w) / (w['Work Time'](w) or 1e-6) + for w in s['Worker Threads'].values()], 0), + 'Worker Threads': {}, + } + logging.statistics["CherryPy HTTPServer %d" % id(self)] = self.stats + + def runtime(self): + if self._start_time is None: + return self._run_time + else: + return self._run_time + (time.time() - self._start_time) + + def __str__(self): + return "%s.%s(%r)" % (self.__module__, self.__class__.__name__, + self.bind_addr) + + def _get_bind_addr(self): + return self._bind_addr + def _set_bind_addr(self, value): + if isinstance(value, tuple) and value[0] in ('', None): + # Despite the socket module docs, using '' does not + # allow AI_PASSIVE to work. Passing None instead + # returns '0.0.0.0' like we want. In other words: + # host AI_PASSIVE result + # '' Y 192.168.x.y + # '' N 192.168.x.y + # None Y 0.0.0.0 + # None N 127.0.0.1 + # But since you can get the same effect with an explicit + # '0.0.0.0', we deny both the empty string and None as values. + raise ValueError("Host values of '' or None are not allowed. " + "Use '0.0.0.0' (IPv4) or '::' (IPv6) instead " + "to listen on all active interfaces.") + self._bind_addr = value + bind_addr = property(_get_bind_addr, _set_bind_addr, + doc="""The interface on which to listen for connections. + + For TCP sockets, a (host, port) tuple. Host values may be any IPv4 + or IPv6 address, or any valid hostname. The string 'localhost' is a + synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6). + The string '0.0.0.0' is a special IPv4 entry meaning "any active + interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for + IPv6. The empty string or None are not allowed. + + For UNIX sockets, supply the filename as a string.""") + + def start(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 = None + + if self.software is None: + self.software = "%s Server" % self.version + + # SSL backward compatibility + if (self.ssl_adapter is None and + getattr(self, 'ssl_certificate', None) and + getattr(self, 'ssl_private_key', None)): + warnings.warn( + "SSL attributes are deprecated in CherryPy 3.2, and will " + "be removed in CherryPy 3.3. Use an ssl_adapter attribute " + "instead.", + DeprecationWarning + ) + try: + from cherrypy.wsgiserver.ssl_pyopenssl import pyOpenSSLAdapter + except ImportError: + pass + else: + self.ssl_adapter = pyOpenSSLAdapter( + self.ssl_certificate, self.ssl_private_key, + getattr(self, 'ssl_certificate_chain', None)) + + # Select the appropriate socket + if isinstance(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, 511) # 0777 + except: pass + + info = [(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_addr + try: + info = socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, 0, socket.AI_PASSIVE) + except socket.gaierror: + if ':' in self.bind_addr[0]: + info = [(socket.AF_INET6, socket.SOCK_STREAM, + 0, "", self.bind_addr + (0, 0))] + else: + info = [(socket.AF_INET, socket.SOCK_STREAM, + 0, "", self.bind_addr)] + + self.socket = None + msg = "No socket could be created" + for res in info: + af, socktype, proto, canonname, sa = res + try: + self.bind(af, socktype, proto) + except socket.error: + if self.socket: + self.socket.close() + self.socket = None + continue + break + if not self.socket: + raise socket.error(msg) + + # Timeout so KeyboardInterrupt can be caught on Win32 + self.socket.settimeout(1) + self.socket.listen(self.request_queue_size) + + # Create worker threads + self.requests.start() + + self.ready = True + self._start_time = time.time() + while self.ready: + try: + self.tick() + except (KeyboardInterrupt, SystemExit): + raise + except: + self.error_log("Error in HTTPServer.tick", level=logging.ERROR, + traceback=True) + + if self.interrupt: + while self.interrupt is True: + # Wait for self.stop() to complete. See _set_interrupt. + time.sleep(0.1) + if self.interrupt: + raise self.interrupt + + def error_log(self, msg="", level=20, traceback=False): + # Override this in subclasses as desired + sys.stderr.write(msg + '\n') + sys.stderr.flush() + if traceback: + tblines = format_exc() + sys.stderr.write(tblines) + sys.stderr.flush() + + def bind(self, family, type, proto=0): + """Create (or recreate) the actual socket object.""" + self.socket = socket.socket(family, type, proto) + prevent_socket_inheritance(self.socket) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if self.nodelay and not isinstance(self.bind_addr, str): + self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + if self.ssl_adapter is not None: + self.socket = self.ssl_adapter.bind(self.socket) + + # If listening on the IPV6 any address ('::' = IN6ADDR_ANY), + # activate dual-stack. See http://www.cherrypy.org/ticket/871. + if (hasattr(socket, 'AF_INET6') and family == socket.AF_INET6 + and self.bind_addr[0] in ('::', '::0', '::0.0.0.0')): + try: + self.socket.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + except (AttributeError, socket.error): + # Apparently, the socket option is not available in + # this machine's TCP stack + pass + + self.socket.bind(self.bind_addr) + + def tick(self): + """Accept a new connection and put it on the Queue.""" + try: + s, addr = self.socket.accept() + if self.stats['Enabled']: + self.stats['Accepts'] += 1 + if not self.ready: + return + + prevent_socket_inheritance(s) + if hasattr(s, 'settimeout'): + s.settimeout(self.timeout) + + makefile = CP_fileobject + ssl_env = {} + # if ssl cert and key are set, we try to be a secure HTTP server + if self.ssl_adapter is not None: + try: + s, ssl_env = self.ssl_adapter.wrap(s) + except NoSSLError: + msg = ("The client sent a plain HTTP request, but " + "this server only speaks HTTPS on this port.") + buf = ["%s 400 Bad Request\r\n" % self.protocol, + "Content-Length: %s\r\n" % len(msg), + "Content-Type: text/plain\r\n\r\n", + msg] + + wfile = makefile(s, "wb", DEFAULT_BUFFER_SIZE) + try: + wfile.sendall("".join(buf)) + except socket.error: + x = sys.exc_info()[1] + if x.args[0] not in socket_errors_to_ignore: + raise + return + if not s: + return + makefile = self.ssl_adapter.makefile + # Re-apply our timeout since we may have a new socket object + if hasattr(s, 'settimeout'): + s.settimeout(self.timeout) + + conn = self.ConnectionClass(self, s, makefile) + + if not isinstance(self.bind_addr, basestring): + # optional values + # Until we do DNS lookups, omit REMOTE_HOST + if addr is None: # sometimes this can happen + # figure out if AF_INET or AF_INET6. + if len(s.getsockname()) == 2: + # AF_INET + addr = ('0.0.0.0', 0) + else: + # AF_INET6 + addr = ('::', 0) + conn.remote_addr = addr[0] + conn.remote_port = addr[1] + + conn.ssl_env = ssl_env + + self.requests.put(conn) + except socket.timeout: + # The only reason for the timeout in start() is so we can + # notice keyboard interrupts on Win32, which don't interrupt + # accept() by default + return + except socket.error: + x = sys.exc_info()[1] + if self.stats['Enabled']: + self.stats['Socket Errors'] += 1 + if x.args[0] in socket_error_eintr: + # I *think* this is right. EINTR should occur when a signal + # is received during the accept() call; all docs say retry + # the call, and I *think* I'm reading it right that Python + # will then go ahead and poll for and handle the signal + # elsewhere. See http://www.cherrypy.org/ticket/707. + return + if x.args[0] in socket_errors_nonblocking: + # Just try again. See http://www.cherrypy.org/ticket/479. + return + if x.args[0] in socket_errors_to_ignore: + # Our socket was closed. + # See http://www.cherrypy.org/ticket/686. + return + raise + + def _get_interrupt(self): + return self._interrupt + def _set_interrupt(self, interrupt): + self._interrupt = True + self.stop() + self._interrupt = interrupt + interrupt = property(_get_interrupt, _set_interrupt, + doc="Set this to an Exception instance to " + "interrupt the server.") + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + self.ready = False + if self._start_time is not None: + self._run_time += (time.time() - self._start_time) + self._start_time = None + + sock = getattr(self, "socket", None) + if sock: + if not isinstance(self.bind_addr, basestring): + # Touch our own socket to make accept() return immediately. + try: + host, port = sock.getsockname()[:2] + except socket.error: + x = sys.exc_info()[1] + if x.args[0] not in socket_errors_to_ignore: + # Changed to use error code and not message + # See http://www.cherrypy.org/ticket/860. + raise + else: + # Note that we're explicitly NOT using AI_PASSIVE, + # here, because we want an actual IP to touch. + # localhost won't work if we've bound to a public IP, + # but it will if we bound to '0.0.0.0' (INADDR_ANY). + for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + s = None + try: + s = socket.socket(af, socktype, proto) + # See http://groups.google.com/group/cherrypy-users/ + # browse_frm/thread/bbfe5eb39c904fe0 + s.settimeout(1.0) + s.connect((host, port)) + s.close() + except socket.error: + if s: + s.close() + if hasattr(sock, "close"): + sock.close() + self.socket = None + + self.requests.stop(self.shutdown_timeout) + + +class Gateway(object): + """A base class to interface HTTPServer with other systems, such as WSGI.""" + + def __init__(self, req): + self.req = req + + def respond(self): + """Process the current request. Must be overridden in a subclass.""" + raise NotImplemented + + +# These may either be wsgiserver.SSLAdapter subclasses or the string names +# of such classes (in which case they will be lazily loaded). +ssl_adapters = { + 'builtin': 'cherrypy.wsgiserver.ssl_builtin.BuiltinSSLAdapter', + 'pyopenssl': 'cherrypy.wsgiserver.ssl_pyopenssl.pyOpenSSLAdapter', + } + +def get_ssl_adapter_class(name='pyopenssl'): + """Return an SSL adapter class for the given name.""" + adapter = ssl_adapters[name.lower()] + if isinstance(adapter, basestring): + last_dot = adapter.rfind(".") + attr_name = adapter[last_dot + 1:] + mod_path = adapter[:last_dot] + + try: + mod = sys.modules[mod_path] + if mod is None: + raise KeyError() + except KeyError: + # The last [''] is important. + mod = __import__(mod_path, globals(), locals(), ['']) + + # Let an AttributeError propagate outward. + try: + adapter = getattr(mod, attr_name) + except AttributeError: + raise AttributeError("'%s' object has no attribute '%s'" + % (mod_path, attr_name)) + + return adapter + +# -------------------------------- WSGI Stuff -------------------------------- # + + +class CherryPyWSGIServer(HTTPServer): + """A subclass of HTTPServer which calls a WSGI application.""" + + wsgi_version = (1, 0) + """The version of WSGI to produce.""" + + def __init__(self, bind_addr, wsgi_app, numthreads=10, server_name=None, + max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5): + self.requests = ThreadPool(self, min=numthreads or 1, max=max) + self.wsgi_app = wsgi_app + self.gateway = wsgi_gateways[self.wsgi_version] + + self.bind_addr = bind_addr + if not server_name: + server_name = socket.gethostname() + self.server_name = server_name + self.request_queue_size = request_queue_size + + self.timeout = timeout + self.shutdown_timeout = shutdown_timeout + self.clear_stats() + + def _get_numthreads(self): + return self.requests.min + def _set_numthreads(self, value): + self.requests.min = value + numthreads = property(_get_numthreads, _set_numthreads) + + +class WSGIGateway(Gateway): + """A base class to interface HTTPServer with WSGI.""" + + def __init__(self, req): + self.req = req + self.started_response = False + self.env = self.get_environ() + self.remaining_bytes_out = None + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version""" + raise NotImplemented + + def respond(self): + """Process the current request.""" + response = self.req.server.wsgi_app(self.env, self.start_response) + try: + for chunk in response: + # "The start_response callable must not actually transmit + # the response headers. Instead, it must store them for the + # server or gateway to transmit only after the first + # iteration of the application return value that yields + # a NON-EMPTY string, or upon the application's first + # invocation of the write() callable." (PEP 333) + if chunk: + if isinstance(chunk, unicodestr): + chunk = chunk.encode('ISO-8859-1') + self.write(chunk) + finally: + if hasattr(response, "close"): + response.close() + + def start_response(self, status, headers, exc_info = None): + """WSGI callable to begin the HTTP response.""" + # "The application may call start_response more than once, + # if and only if the exc_info argument is provided." + if self.started_response and not exc_info: + raise AssertionError("WSGI start_response called a second " + "time with no exc_info.") + self.started_response = True + + # "if exc_info is provided, and the HTTP headers have already been + # sent, start_response must raise an error, and should raise the + # exc_info tuple." + if self.req.sent_headers: + try: + raise exc_info[0], exc_info[1], exc_info[2] + finally: + exc_info = None + + self.req.status = status + for k, v in headers: + if not isinstance(k, str): + raise TypeError("WSGI response header key %r is not of type str." % k) + if not isinstance(v, str): + raise TypeError("WSGI response header value %r is not of type str." % v) + if k.lower() == 'content-length': + self.remaining_bytes_out = int(v) + self.req.outheaders.extend(headers) + + return self.write + + def write(self, chunk): + """WSGI callable to write unbuffered data to the client. + + This method is also used internally by start_response (to write + data from the iterable returned by the WSGI application). + """ + if not self.started_response: + raise AssertionError("WSGI write called before start_response.") + + chunklen = len(chunk) + rbo = self.remaining_bytes_out + if rbo is not None and chunklen > rbo: + if not self.req.sent_headers: + # Whew. We can send a 500 to the client. + self.req.simple_response("500 Internal Server Error", + "The requested resource returned more bytes than the " + "declared Content-Length.") + else: + # Dang. We have probably already sent data. Truncate the chunk + # to fit (so the client doesn't hang) and raise an error later. + chunk = chunk[:rbo] + + if not self.req.sent_headers: + self.req.sent_headers = True + self.req.send_headers() + + self.req.write(chunk) + + if rbo is not None: + rbo -= chunklen + if rbo < 0: + raise ValueError( + "Response body exceeds the declared Content-Length.") + + +class WSGIGateway_10(WSGIGateway): + """A Gateway class to interface HTTPServer with WSGI 1.0.x.""" + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version""" + req = self.req + env = { + # 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. + 'ACTUAL_SERVER_PROTOCOL': req.server.protocol, + 'PATH_INFO': req.path, + 'QUERY_STRING': req.qs, + 'REMOTE_ADDR': req.conn.remote_addr or '', + 'REMOTE_PORT': str(req.conn.remote_port or ''), + 'REQUEST_METHOD': req.method, + 'REQUEST_URI': req.uri, + 'SCRIPT_NAME': '', + 'SERVER_NAME': req.server.server_name, + # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol. + 'SERVER_PROTOCOL': req.request_protocol, + 'SERVER_SOFTWARE': req.server.software, + 'wsgi.errors': sys.stderr, + 'wsgi.input': req.rfile, + 'wsgi.multiprocess': False, + 'wsgi.multithread': True, + 'wsgi.run_once': False, + 'wsgi.url_scheme': req.scheme, + 'wsgi.version': (1, 0), + } + + if isinstance(req.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. + env["SERVER_PORT"] = "" + else: + env["SERVER_PORT"] = str(req.server.bind_addr[1]) + + # Request headers + for k, v in req.inheaders.iteritems(): + env["HTTP_" + k.upper().replace("-", "_")] = v + + # CONTENT_TYPE/CONTENT_LENGTH + ct = env.pop("HTTP_CONTENT_TYPE", None) + if ct is not None: + env["CONTENT_TYPE"] = ct + cl = env.pop("HTTP_CONTENT_LENGTH", None) + if cl is not None: + env["CONTENT_LENGTH"] = cl + + if req.conn.ssl_env: + env.update(req.conn.ssl_env) + + return env + + +class WSGIGateway_u0(WSGIGateway_10): + """A Gateway class to interface HTTPServer with WSGI u.0. + + WSGI u.0 is an experimental protocol, which uses unicode for keys and values + in both Python 2 and Python 3. + """ + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version""" + req = self.req + env_10 = WSGIGateway_10.get_environ(self) + env = dict([(k.decode('ISO-8859-1'), v) for k, v in env_10.iteritems()]) + env[u'wsgi.version'] = ('u', 0) + + # Request-URI + env.setdefault(u'wsgi.url_encoding', u'utf-8') + try: + for key in [u"PATH_INFO", u"SCRIPT_NAME", u"QUERY_STRING"]: + env[key] = env_10[str(key)].decode(env[u'wsgi.url_encoding']) + except UnicodeDecodeError: + # Fall back to latin 1 so apps can transcode if needed. + env[u'wsgi.url_encoding'] = u'ISO-8859-1' + for key in [u"PATH_INFO", u"SCRIPT_NAME", u"QUERY_STRING"]: + env[key] = env_10[str(key)].decode(env[u'wsgi.url_encoding']) + + for k, v in sorted(env.items()): + if isinstance(v, str) and k not in ('REQUEST_URI', 'wsgi.input'): + env[k] = v.decode('ISO-8859-1') + + return env + +wsgi_gateways = { + (1, 0): WSGIGateway_10, + ('u', 0): WSGIGateway_u0, +} + +class WSGIPathInfoDispatcher(object): + """A WSGI dispatcher for dispatch based on the PATH_INFO. + + apps: a dict or list of (path_prefix, app) pairs. + """ + + def __init__(self, apps): + try: + apps = list(apps.items()) + except AttributeError: + pass + + # Sort the apps by len(path), descending + apps.sort(cmp=lambda x,y: cmp(len(x[0]), len(y[0]))) + apps.reverse() + + # The path_prefix strings must start, but not end, with a slash. + # Use "" instead of "/". + self.apps = [(p.rstrip("/"), a) for p, a in apps] + + def __call__(self, environ, start_response): + path = environ["PATH_INFO"] or "/" + for p, app in self.apps: + # The apps list should be sorted by length, descending. + if path.startswith(p + "/") or path == p: + environ = environ.copy() + environ["SCRIPT_NAME"] = environ["SCRIPT_NAME"] + p + environ["PATH_INFO"] = path[len(p):] + return app(environ, start_response) + + start_response('404 Not Found', [('Content-Type', 'text/plain'), + ('Content-Length', '0')]) + return [''] + diff --git a/src/__init__.py b/src/__init__.py index e69de29..80d200b 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- + +""" + General configuration and imports gathering. +""" + +import os +import sys + +import settings +from paste import Paste + + +def setup_path(): + """ + Add the project dir in the python path to the site to run with the + source code beeing just copied/pasted and not installed. + + Also try to import dependancies. If it fails, fallback on embeded libs. + """ + sys.path.insert(0, os.path.dirname(settings.ROOT_DIR)) + + try: + import bottle + except ImportError: + sys.path.append(os.path.join(settings.ROOT_DIR, 'libs')) \ No newline at end of file diff --git a/start.py b/start.py index 345a415..cd806c7 100644 --- a/start.py +++ b/start.py @@ -1,18 +1,20 @@ # -*- coding: utf-8 -*- +""" + Main script including controller, rooting, dependancy management, and + server run. +""" + import os import hashlib -import sys + +from src import settings, setup_path, Paste + +setup_path() from bottle import (Bottle, route, run, abort, static_file, debug, view, request) -import settings - -# ensure we got the project module on the python path to avoid import problems -sys.path.insert(0, os.path.dirname(settings.ROOT_DIR)) - -from src.paste import Paste app = Bottle() From 1a46d998ac88041bd82cfd5fffa9bea1e7cd8576 Mon Sep 17 00:00:00 2001 From: sam Date: Fri, 27 Apr 2012 04:41:20 +0700 Subject: [PATCH 6/6] Expiration and burn after reading works --- libs/cherrypy/scaffold/__init__.py | 61 -- libs/cherrypy/scaffold/example.conf | 3 - libs/cherrypy/scaffold/site.conf | 14 - .../static/made_with_cherrypy_small.png | Bin 7455 -> 0 bytes libs/cherrypy/test/__init__.py | 27 - libs/cherrypy/test/_test_decorators.py | 41 - libs/cherrypy/test/_test_states_demo.py | 66 -- libs/cherrypy/test/benchmark.py | 409 ---------- libs/cherrypy/test/checkerdemo.py | 47 -- libs/cherrypy/test/helper.py | 494 ------------ libs/cherrypy/test/logtest.py | 188 ----- libs/cherrypy/test/modfastcgi.py | 135 ---- libs/cherrypy/test/modfcgid.py | 125 --- libs/cherrypy/test/modpy.py | 163 ---- libs/cherrypy/test/modwsgi.py | 148 ---- libs/cherrypy/test/sessiondemo.py | 153 ---- libs/cherrypy/test/static/dirback.jpg | Bin 18238 -> 0 bytes libs/cherrypy/test/static/index.html | 1 - libs/cherrypy/test/style.css | 1 - libs/cherrypy/test/test.pem | 38 - libs/cherrypy/test/test_auth_basic.py | 79 -- libs/cherrypy/test/test_auth_digest.py | 115 --- libs/cherrypy/test/test_bus.py | 263 ------- libs/cherrypy/test/test_caching.py | 328 -------- libs/cherrypy/test/test_config.py | 256 ------ libs/cherrypy/test/test_config_server.py | 121 --- libs/cherrypy/test/test_conn.py | 734 ----------------- libs/cherrypy/test/test_core.py | 688 ---------------- .../test/test_dynamicobjectmapping.py | 404 ---------- libs/cherrypy/test/test_encoding.py | 363 --------- libs/cherrypy/test/test_etags.py | 83 -- libs/cherrypy/test/test_http.py | 212 ----- libs/cherrypy/test/test_httpauth.py | 151 ---- libs/cherrypy/test/test_httplib.py | 29 - libs/cherrypy/test/test_json.py | 79 -- libs/cherrypy/test/test_logging.py | 157 ---- libs/cherrypy/test/test_mime.py | 128 --- libs/cherrypy/test/test_misc_tools.py | 207 ----- libs/cherrypy/test/test_objectmapping.py | 404 ---------- libs/cherrypy/test/test_proxy.py | 129 --- libs/cherrypy/test/test_refleaks.py | 59 -- libs/cherrypy/test/test_request_obj.py | 737 ------------------ libs/cherrypy/test/test_routes.py | 69 -- libs/cherrypy/test/test_session.py | 464 ----------- .../cherrypy/test/test_sessionauthenticate.py | 62 -- libs/cherrypy/test/test_states.py | 439 ----------- libs/cherrypy/test/test_static.py | 300 ------- libs/cherrypy/test/test_tools.py | 399 ---------- libs/cherrypy/test/test_tutorials.py | 201 ----- libs/cherrypy/test/test_virtualhost.py | 107 --- libs/cherrypy/test/test_wsgi_ns.py | 91 --- libs/cherrypy/test/test_wsgi_vhost.py | 36 - libs/cherrypy/test/test_wsgiapps.py | 118 --- libs/cherrypy/test/test_xmlrpc.py | 179 ----- libs/cherrypy/test/webtest.py | 575 -------------- libs/cherrypy/tutorial/README.txt | 16 - libs/cherrypy/tutorial/__init__.py | 3 - libs/cherrypy/tutorial/bonus-sqlobject.py | 168 ---- libs/cherrypy/tutorial/custom_error.html | 14 - libs/cherrypy/tutorial/pdf_file.pdf | Bin 85698 -> 0 bytes libs/cherrypy/tutorial/tut01_helloworld.py | 35 - .../cherrypy/tutorial/tut02_expose_methods.py | 32 - libs/cherrypy/tutorial/tut03_get_and_post.py | 53 -- libs/cherrypy/tutorial/tut04_complex_site.py | 98 --- .../tutorial/tut05_derived_objects.py | 83 -- .../cherrypy/tutorial/tut06_default_method.py | 64 -- libs/cherrypy/tutorial/tut07_sessions.py | 44 -- .../tutorial/tut08_generators_and_yield.py | 47 -- libs/cherrypy/tutorial/tut09_files.py | 107 --- libs/cherrypy/tutorial/tut10_http_errors.py | 81 -- libs/cherrypy/tutorial/tutorial.conf | 4 - settings.py | 17 +- src/paste.py | 28 +- start.py | 40 +- static/js/behavior.js | 4 +- views/paste.tpl | 12 + 76 files changed, 80 insertions(+), 11750 deletions(-) delete mode 100644 libs/cherrypy/scaffold/__init__.py delete mode 100644 libs/cherrypy/scaffold/example.conf delete mode 100644 libs/cherrypy/scaffold/site.conf delete mode 100644 libs/cherrypy/scaffold/static/made_with_cherrypy_small.png delete mode 100644 libs/cherrypy/test/__init__.py delete mode 100644 libs/cherrypy/test/_test_decorators.py delete mode 100644 libs/cherrypy/test/_test_states_demo.py delete mode 100644 libs/cherrypy/test/benchmark.py delete mode 100644 libs/cherrypy/test/checkerdemo.py delete mode 100644 libs/cherrypy/test/helper.py delete mode 100644 libs/cherrypy/test/logtest.py delete mode 100644 libs/cherrypy/test/modfastcgi.py delete mode 100644 libs/cherrypy/test/modfcgid.py delete mode 100644 libs/cherrypy/test/modpy.py delete mode 100644 libs/cherrypy/test/modwsgi.py delete mode 100644 libs/cherrypy/test/sessiondemo.py delete mode 100644 libs/cherrypy/test/static/dirback.jpg delete mode 100644 libs/cherrypy/test/static/index.html delete mode 100644 libs/cherrypy/test/style.css delete mode 100644 libs/cherrypy/test/test.pem delete mode 100644 libs/cherrypy/test/test_auth_basic.py delete mode 100644 libs/cherrypy/test/test_auth_digest.py delete mode 100644 libs/cherrypy/test/test_bus.py delete mode 100644 libs/cherrypy/test/test_caching.py delete mode 100644 libs/cherrypy/test/test_config.py delete mode 100644 libs/cherrypy/test/test_config_server.py delete mode 100644 libs/cherrypy/test/test_conn.py delete mode 100644 libs/cherrypy/test/test_core.py delete mode 100644 libs/cherrypy/test/test_dynamicobjectmapping.py delete mode 100644 libs/cherrypy/test/test_encoding.py delete mode 100644 libs/cherrypy/test/test_etags.py delete mode 100644 libs/cherrypy/test/test_http.py delete mode 100644 libs/cherrypy/test/test_httpauth.py delete mode 100644 libs/cherrypy/test/test_httplib.py delete mode 100644 libs/cherrypy/test/test_json.py delete mode 100644 libs/cherrypy/test/test_logging.py delete mode 100644 libs/cherrypy/test/test_mime.py delete mode 100644 libs/cherrypy/test/test_misc_tools.py delete mode 100644 libs/cherrypy/test/test_objectmapping.py delete mode 100644 libs/cherrypy/test/test_proxy.py delete mode 100644 libs/cherrypy/test/test_refleaks.py delete mode 100644 libs/cherrypy/test/test_request_obj.py delete mode 100644 libs/cherrypy/test/test_routes.py delete mode 100644 libs/cherrypy/test/test_session.py delete mode 100644 libs/cherrypy/test/test_sessionauthenticate.py delete mode 100644 libs/cherrypy/test/test_states.py delete mode 100644 libs/cherrypy/test/test_static.py delete mode 100644 libs/cherrypy/test/test_tools.py delete mode 100644 libs/cherrypy/test/test_tutorials.py delete mode 100644 libs/cherrypy/test/test_virtualhost.py delete mode 100644 libs/cherrypy/test/test_wsgi_ns.py delete mode 100644 libs/cherrypy/test/test_wsgi_vhost.py delete mode 100644 libs/cherrypy/test/test_wsgiapps.py delete mode 100644 libs/cherrypy/test/test_xmlrpc.py delete mode 100644 libs/cherrypy/test/webtest.py delete mode 100644 libs/cherrypy/tutorial/README.txt delete mode 100644 libs/cherrypy/tutorial/__init__.py delete mode 100644 libs/cherrypy/tutorial/bonus-sqlobject.py delete mode 100644 libs/cherrypy/tutorial/custom_error.html delete mode 100644 libs/cherrypy/tutorial/pdf_file.pdf delete mode 100644 libs/cherrypy/tutorial/tut01_helloworld.py delete mode 100644 libs/cherrypy/tutorial/tut02_expose_methods.py delete mode 100644 libs/cherrypy/tutorial/tut03_get_and_post.py delete mode 100644 libs/cherrypy/tutorial/tut04_complex_site.py delete mode 100644 libs/cherrypy/tutorial/tut05_derived_objects.py delete mode 100644 libs/cherrypy/tutorial/tut06_default_method.py delete mode 100644 libs/cherrypy/tutorial/tut07_sessions.py delete mode 100644 libs/cherrypy/tutorial/tut08_generators_and_yield.py delete mode 100644 libs/cherrypy/tutorial/tut09_files.py delete mode 100644 libs/cherrypy/tutorial/tut10_http_errors.py delete mode 100644 libs/cherrypy/tutorial/tutorial.conf diff --git a/libs/cherrypy/scaffold/__init__.py b/libs/cherrypy/scaffold/__init__.py deleted file mode 100644 index 00964ac..0000000 --- a/libs/cherrypy/scaffold/__init__.py +++ /dev/null @@ -1,61 +0,0 @@ -""", a CherryPy application. - -Use this as a base for creating new CherryPy applications. When you want -to make a new app, copy and paste this folder to some other location -(maybe site-packages) and rename it to the name of your project, -then tweak as desired. - -Even before any tweaking, this should serve a few demonstration pages. -Change to this directory and run: - - ../cherryd -c site.conf - -""" - -import cherrypy -from cherrypy import tools, url - -import os -local_dir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - - -class Root: - - _cp_config = {'tools.log_tracebacks.on': True, - } - - def index(self): - return """ -Try some other path, -or a default path.
-Or, just look at the pretty picture:
- -""" % (url("other"), url("else"), - url("files/made_with_cherrypy_small.png")) - index.exposed = True - - def default(self, *args, **kwargs): - return "args: %s kwargs: %s" % (args, kwargs) - default.exposed = True - - def other(self, a=2, b='bananas', c=None): - cherrypy.response.headers['Content-Type'] = 'text/plain' - if c is None: - return "Have %d %s." % (int(a), b) - else: - return "Have %d %s, %s." % (int(a), b, c) - other.exposed = True - - files = cherrypy.tools.staticdir.handler( - section="/files", - dir=os.path.join(local_dir, "static"), - # Ignore .php files, etc. - match=r'\.(css|gif|html?|ico|jpe?g|js|png|swf|xml)$', - ) - - -root = Root() - -# Uncomment the following to use your own favicon instead of CP's default. -#favicon_path = os.path.join(local_dir, "favicon.ico") -#root.favicon_ico = tools.staticfile.handler(filename=favicon_path) diff --git a/libs/cherrypy/scaffold/example.conf b/libs/cherrypy/scaffold/example.conf deleted file mode 100644 index 93a6e53..0000000 --- a/libs/cherrypy/scaffold/example.conf +++ /dev/null @@ -1,3 +0,0 @@ -[/] -log.error_file: "error.log" -log.access_file: "access.log" \ No newline at end of file diff --git a/libs/cherrypy/scaffold/site.conf b/libs/cherrypy/scaffold/site.conf deleted file mode 100644 index 6ed3898..0000000 --- a/libs/cherrypy/scaffold/site.conf +++ /dev/null @@ -1,14 +0,0 @@ -[global] -# Uncomment this when you're done developing -#environment: "production" - -server.socket_host: "0.0.0.0" -server.socket_port: 8088 - -# Uncomment the following lines to run on HTTPS at the same time -#server.2.socket_host: "0.0.0.0" -#server.2.socket_port: 8433 -#server.2.ssl_certificate: '../test/test.pem' -#server.2.ssl_private_key: '../test/test.pem' - -tree.myapp: cherrypy.Application(scaffold.root, "/", "example.conf") diff --git a/libs/cherrypy/scaffold/static/made_with_cherrypy_small.png b/libs/cherrypy/scaffold/static/made_with_cherrypy_small.png deleted file mode 100644 index c3aafeed952190f5da9982bb359aa75b107ff079..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7455 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ξb^d7?(E1>b^p1aAHqz^4_gi*iN74 z2x=0ye3f^&T`3}*uTMEcbiU`b9g;sI;+6KiNcf)^`nsXi-frS=FWtBM%%{s+q-b~@ zzS-J5?eXc=Mqf16M22y!^XWR56S3YVRDA#0Kl8L_{WROWyuac4&9f(VI|qvL{&;>o zQ^7W9=JKg3?~nUWX64(uQZJH2u;|4KooD~9?6_FVx;=P>k7}$)#Qr3m_22Dsxihr> zv|T9>yK-DFnB!N_oF%JJ!*fFDQ@2>}*OR}b4JwW1pG%T?ajc)^S6)Qh*MrZucYb(f$zc93 zf{TBt{c8u+x;tGjgWP9q7i;YLH^nKNZ(-!k5ZUJGY}4ZBzc08x^WEuGqodw_3m4u_ zc)Y9S-QJfbE5oO(=xr@#aYzgfT_0z=q2Qj$o7>ul&23DlTu(l{>*|HADUj(SE( z*t;>f3C~ScdKz$ZbHisTyDjXD-RoMeo{aL?cmE1!(_fWs+IEG9G;*d#bM@9{FVNY~ zHrwn^ji!CZ+U%Iy>xopSK*yCXEM+4 zbhcZ-5xcBSTPZKkyn{$)^^OPTkkHs}Lw%J>LEApQ4 z=%=sK6PdBTE&hw5ja+LZZKBuaE&v*BX457w2ur$sJ?uiRB` zx9``Y)df=P@*jV_o_SR5=rVuhIQ92^TDjZ4Je|w*ywjDH3n0zB+HP><-J_d+zZ|tC{w>ffl}hCjLsfq|<+> zqVz}XozCO^D!;u=l#=gB?g;wI^YH%n>drOc4O2um`P=i~nEvIEQ}*f5R4FC(YaM)3 zuReIRLHgXc-4EU^>S-xRj`E$woxIIEsQ2ZE>yzTS#jmnOTWX z2{*4F*eP6HVJ+^b`5=wquKJmrnB^*0r+i+uAeXQ3uV${}Jn`3a%Okq|?Ot*z&z+i7 zX|&V(<(35|ce`#)vzfL(G1~B*h~MAGE)}P|yRSLlJvw=kN@+{XMCY)q6)xAjmK@EB z_?(z0cjI5!oolgMB(<)xq`bMI7U|4hzWBVNZp=Kt3z2tBxqrT0F#W1y^^=mBzt}Z{ zt}wY?-}CZN*56kP#401dEbn|#_t171#{oV5Eum+={x`aBbZPeN%V}q3S@H&%91Z)F zx$9}S{94_^NspTKa-2Kfu+5RtYBjp0QajOIGxmZ9_xYi`$B72R>-M6(* ze{q-GWIgXJqOG=@^Pb4+r@Oq_W(IQYncH*1Ho@?%`Q9f5XDel$_M8m}oOWBwJ2d{s zRHoJQG;crOFNe}-& zMX^1WD^EndzT}tBule?OTHYRMnd^|F5<6cx{#ByyQ*XvqzKvh|Rese+{Jgn2-N4+q zwae(Q<>J-vDt>Kw`!A{F&CJUmE?jeaa - - CherryPy Benchmark - - - - -""" - index.exposed = True - - def hello(self): - return "Hello, world\r\n" - hello.exposed = True - - def sizer(self, size): - resp = size_cache.get(size, None) - if resp is None: - size_cache[size] = resp = "X" * int(size) - return resp - sizer.exposed = True - - -cherrypy.config.update({ - 'log.error.file': '', - 'environment': 'production', - 'server.socket_host': '127.0.0.1', - 'server.socket_port': 54583, - 'server.max_request_header_size': 0, - 'server.max_request_body_size': 0, - 'engine.deadlock_poll_freq': 0, - }) - -# Cheat mode on ;) -del cherrypy.config['tools.log_tracebacks.on'] -del cherrypy.config['tools.log_headers.on'] -del cherrypy.config['tools.trailing_slash.on'] - -appconf = { - '/static': { - 'tools.staticdir.on': True, - 'tools.staticdir.dir': 'static', - 'tools.staticdir.root': curdir, - }, - } -app = cherrypy.tree.mount(Root(), SCRIPT_NAME, appconf) - - -class NullRequest: - """A null HTTP request class, returning 200 and an empty body.""" - - def __init__(self, local, remote, scheme="http"): - pass - - def close(self): - pass - - def run(self, method, path, query_string, protocol, headers, rfile): - cherrypy.response.status = "200 OK" - cherrypy.response.header_list = [("Content-Type", 'text/html'), - ("Server", "Null CherryPy"), - ("Date", httputil.HTTPDate()), - ("Content-Length", "0"), - ] - cherrypy.response.body = [""] - return cherrypy.response - - -class NullResponse: - pass - - -class ABSession: - """A session of 'ab', the Apache HTTP server benchmarking tool. - -Example output from ab: - -This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.1 $> apache-2.0 -Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ -Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/ - -Benchmarking 127.0.0.1 (be patient) -Completed 100 requests -Completed 200 requests -Completed 300 requests -Completed 400 requests -Completed 500 requests -Completed 600 requests -Completed 700 requests -Completed 800 requests -Completed 900 requests - - -Server Software: CherryPy/3.1beta -Server Hostname: 127.0.0.1 -Server Port: 54583 - -Document Path: /static/index.html -Document Length: 14 bytes - -Concurrency Level: 10 -Time taken for tests: 9.643867 seconds -Complete requests: 1000 -Failed requests: 0 -Write errors: 0 -Total transferred: 189000 bytes -HTML transferred: 14000 bytes -Requests per second: 103.69 [#/sec] (mean) -Time per request: 96.439 [ms] (mean) -Time per request: 9.644 [ms] (mean, across all concurrent requests) -Transfer rate: 19.08 [Kbytes/sec] received - -Connection Times (ms) - min mean[+/-sd] median max -Connect: 0 0 2.9 0 10 -Processing: 20 94 7.3 90 130 -Waiting: 0 43 28.1 40 100 -Total: 20 95 7.3 100 130 - -Percentage of the requests served within a certain time (ms) - 50% 100 - 66% 100 - 75% 100 - 80% 100 - 90% 100 - 95% 100 - 98% 100 - 99% 110 - 100% 130 (longest request) -Finished 1000 requests -""" - - parse_patterns = [('complete_requests', 'Completed', - ntob(r'^Complete requests:\s*(\d+)')), - ('failed_requests', 'Failed', - ntob(r'^Failed requests:\s*(\d+)')), - ('requests_per_second', 'req/sec', - ntob(r'^Requests per second:\s*([0-9.]+)')), - ('time_per_request_concurrent', 'msec/req', - ntob(r'^Time per request:\s*([0-9.]+).*concurrent requests\)$')), - ('transfer_rate', 'KB/sec', - ntob(r'^Transfer rate:\s*([0-9.]+)')), - ] - - def __init__(self, path=SCRIPT_NAME + "/hello", requests=1000, concurrency=10): - self.path = path - self.requests = requests - self.concurrency = concurrency - - def args(self): - port = cherrypy.server.socket_port - assert self.concurrency > 0 - assert self.requests > 0 - # Don't use "localhost". - # Cf http://mail.python.org/pipermail/python-win32/2008-March/007050.html - return ("-k -n %s -c %s http://127.0.0.1:%s%s" % - (self.requests, self.concurrency, port, self.path)) - - def run(self): - # Parse output of ab, setting attributes on self - try: - self.output = _cpmodpy.read_process(AB_PATH or "ab", self.args()) - except: - print(_cperror.format_exc()) - raise - - for attr, name, pattern in self.parse_patterns: - val = re.search(pattern, self.output, re.MULTILINE) - if val: - val = val.group(1) - setattr(self, attr, val) - else: - setattr(self, attr, None) - - -safe_threads = (25, 50, 100, 200, 400) -if sys.platform in ("win32",): - # For some reason, ab crashes with > 50 threads on my Win2k laptop. - safe_threads = (10, 20, 30, 40, 50) - - -def thread_report(path=SCRIPT_NAME + "/hello", concurrency=safe_threads): - sess = ABSession(path) - attrs, names, patterns = list(zip(*sess.parse_patterns)) - avg = dict.fromkeys(attrs, 0.0) - - yield ('threads',) + names - for c in concurrency: - sess.concurrency = c - sess.run() - row = [c] - for attr in attrs: - val = getattr(sess, attr) - if val is None: - print(sess.output) - row = None - break - val = float(val) - avg[attr] += float(val) - row.append(val) - if row: - yield row - - # Add a row of averages. - yield ["Average"] + [str(avg[attr] / len(concurrency)) for attr in attrs] - -def size_report(sizes=(10, 100, 1000, 10000, 100000, 100000000), - concurrency=50): - sess = ABSession(concurrency=concurrency) - attrs, names, patterns = list(zip(*sess.parse_patterns)) - yield ('bytes',) + names - for sz in sizes: - sess.path = "%s/sizer?size=%s" % (SCRIPT_NAME, sz) - sess.run() - yield [sz] + [getattr(sess, attr) for attr in attrs] - -def print_report(rows): - for row in rows: - print("") - for i, val in enumerate(row): - sys.stdout.write(str(val).rjust(10) + " | ") - print("") - - -def run_standard_benchmarks(): - print("") - print("Client Thread Report (1000 requests, 14 byte response body, " - "%s server threads):" % cherrypy.server.thread_pool) - print_report(thread_report()) - - print("") - print("Client Thread Report (1000 requests, 14 bytes via staticdir, " - "%s server threads):" % cherrypy.server.thread_pool) - print_report(thread_report("%s/static/index.html" % SCRIPT_NAME)) - - print("") - print("Size Report (1000 requests, 50 client threads, " - "%s server threads):" % cherrypy.server.thread_pool) - print_report(size_report()) - - -# modpython and other WSGI # - -def startup_modpython(req=None): - """Start the CherryPy app server in 'serverless' mode (for modpython/WSGI).""" - if cherrypy.engine.state == cherrypy._cpengine.STOPPED: - if req: - if "nullreq" in req.get_options(): - cherrypy.engine.request_class = NullRequest - cherrypy.engine.response_class = NullResponse - ab_opt = req.get_options().get("ab", "") - if ab_opt: - global AB_PATH - AB_PATH = ab_opt - cherrypy.engine.start() - if cherrypy.engine.state == cherrypy._cpengine.STARTING: - cherrypy.engine.wait() - return 0 # apache.OK - - -def run_modpython(use_wsgi=False): - print("Starting mod_python...") - pyopts = [] - - # Pass the null and ab=path options through Apache - if "--null" in opts: - pyopts.append(("nullreq", "")) - - if "--ab" in opts: - pyopts.append(("ab", opts["--ab"])) - - s = _cpmodpy.ModPythonServer - if use_wsgi: - pyopts.append(("wsgi.application", "cherrypy::tree")) - pyopts.append(("wsgi.startup", "cherrypy.test.benchmark::startup_modpython")) - handler = "modpython_gateway::handler" - s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH, handler=handler) - else: - pyopts.append(("cherrypy.setup", "cherrypy.test.benchmark::startup_modpython")) - s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH) - - try: - s.start() - run() - finally: - s.stop() - - - -if __name__ == '__main__': - longopts = ['cpmodpy', 'modpython', 'null', 'notests', - 'help', 'ab=', 'apache='] - try: - switches, args = getopt.getopt(sys.argv[1:], "", longopts) - opts = dict(switches) - except getopt.GetoptError: - print(__doc__) - sys.exit(2) - - if "--help" in opts: - print(__doc__) - sys.exit(0) - - if "--ab" in opts: - AB_PATH = opts['--ab'] - - if "--notests" in opts: - # Return without stopping the server, so that the pages - # can be tested from a standard web browser. - def run(): - port = cherrypy.server.socket_port - print("You may now open http://127.0.0.1:%s%s/" % - (port, SCRIPT_NAME)) - - if "--null" in opts: - print("Using null Request object") - else: - def run(): - end = time.time() - start - print("Started in %s seconds" % end) - if "--null" in opts: - print("\nUsing null Request object") - try: - try: - run_standard_benchmarks() - except: - print(_cperror.format_exc()) - raise - finally: - cherrypy.engine.exit() - - print("Starting CherryPy app server...") - - class NullWriter(object): - """Suppresses the printing of socket errors.""" - def write(self, data): - pass - sys.stderr = NullWriter() - - start = time.time() - - if "--cpmodpy" in opts: - run_modpython() - elif "--modpython" in opts: - run_modpython(use_wsgi=True) - else: - if "--null" in opts: - cherrypy.server.request_class = NullRequest - cherrypy.server.response_class = NullResponse - - cherrypy.engine.start_with_callback(run) - cherrypy.engine.block() diff --git a/libs/cherrypy/test/checkerdemo.py b/libs/cherrypy/test/checkerdemo.py deleted file mode 100644 index 32a7dee..0000000 --- a/libs/cherrypy/test/checkerdemo.py +++ /dev/null @@ -1,47 +0,0 @@ -"""Demonstration app for cherrypy.checker. - -This application is intentionally broken and badly designed. -To demonstrate the output of the CherryPy Checker, simply execute -this module. -""" - -import os -import cherrypy -thisdir = os.path.dirname(os.path.abspath(__file__)) - -class Root: - pass - -if __name__ == '__main__': - conf = {'/base': {'tools.staticdir.root': thisdir, - # Obsolete key. - 'throw_errors': True, - }, - # This entry should be OK. - '/base/static': {'tools.staticdir.on': True, - 'tools.staticdir.dir': 'static'}, - # Warn on missing folder. - '/base/js': {'tools.staticdir.on': True, - 'tools.staticdir.dir': 'js'}, - # Warn on dir with an abs path even though we provide root. - '/base/static2': {'tools.staticdir.on': True, - 'tools.staticdir.dir': '/static'}, - # Warn on dir with a relative path with no root. - '/static3': {'tools.staticdir.on': True, - 'tools.staticdir.dir': 'static'}, - # Warn on unknown namespace - '/unknown': {'toobles.gzip.on': True}, - # Warn special on cherrypy..* - '/cpknown': {'cherrypy.tools.encode.on': True}, - # Warn on mismatched types - '/conftype': {'request.show_tracebacks': 14}, - # Warn on unknown tool. - '/web': {'tools.unknown.on': True}, - # Warn on server.* in app config. - '/app1': {'server.socket_host': '0.0.0.0'}, - # Warn on 'localhost' - 'global': {'server.socket_host': 'localhost'}, - # Warn on '[name]' - '[/extra_brackets]': {}, - } - cherrypy.quickstart(Root(), config=conf) diff --git a/libs/cherrypy/test/helper.py b/libs/cherrypy/test/helper.py deleted file mode 100644 index e3006a5..0000000 --- a/libs/cherrypy/test/helper.py +++ /dev/null @@ -1,494 +0,0 @@ -"""A library of helper functions for the CherryPy test suite.""" - -import datetime -import logging -log = logging.getLogger(__name__) -import os -thisdir = os.path.abspath(os.path.dirname(__file__)) -serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem') - -import re -import sys -import time -import warnings - -import cherrypy -from cherrypy._cpcompat import basestring, copyitems, HTTPSConnection, ntob -from cherrypy.lib import httputil -from cherrypy.lib import gctools -from cherrypy.lib.reprconf import unrepr -from cherrypy.test import webtest - -import nose - -_testconfig = None - -def get_tst_config(overconf = {}): - global _testconfig - if _testconfig is None: - conf = { - 'scheme': 'http', - 'protocol': "HTTP/1.1", - 'port': 54583, - 'host': '127.0.0.1', - 'validate': False, - 'conquer': False, - 'server': 'wsgi', - } - try: - import testconfig - _conf = testconfig.config.get('supervisor', None) - if _conf is not None: - for k, v in _conf.items(): - if isinstance(v, basestring): - _conf[k] = unrepr(v) - conf.update(_conf) - except ImportError: - pass - _testconfig = conf - conf = _testconfig.copy() - conf.update(overconf) - - return conf - -class Supervisor(object): - """Base class for modeling and controlling servers during testing.""" - - def __init__(self, **kwargs): - for k, v in kwargs.items(): - if k == 'port': - setattr(self, k, int(v)) - setattr(self, k, v) - - -log_to_stderr = lambda msg, level: sys.stderr.write(msg + os.linesep) - -class LocalSupervisor(Supervisor): - """Base class for modeling/controlling servers which run in the same process. - - When the server side runs in a different process, start/stop can dump all - state between each test module easily. When the server side runs in the - same process as the client, however, we have to do a bit more work to ensure - config and mounted apps are reset between tests. - """ - - using_apache = False - using_wsgi = False - - def __init__(self, **kwargs): - for k, v in kwargs.items(): - setattr(self, k, v) - - cherrypy.server.httpserver = self.httpserver_class - - # This is perhaps the wrong place for this call but this is the only - # place that i've found so far that I KNOW is early enough to set this. - cherrypy.config.update({'log.screen': False}) - engine = cherrypy.engine - if hasattr(engine, "signal_handler"): - engine.signal_handler.subscribe() - if hasattr(engine, "console_control_handler"): - engine.console_control_handler.subscribe() - #engine.subscribe('log', log_to_stderr) - - def start(self, modulename=None): - """Load and start the HTTP server.""" - if modulename: - # Unhook httpserver so cherrypy.server.start() creates a new - # one (with config from setup_server, if declared). - cherrypy.server.httpserver = None - - cherrypy.engine.start() - - self.sync_apps() - - def sync_apps(self): - """Tell the server about any apps which the setup functions mounted.""" - pass - - def stop(self): - td = getattr(self, 'teardown', None) - if td: - td() - - cherrypy.engine.exit() - - for name, server in copyitems(getattr(cherrypy, 'servers', {})): - server.unsubscribe() - del cherrypy.servers[name] - - -class NativeServerSupervisor(LocalSupervisor): - """Server supervisor for the builtin HTTP server.""" - - httpserver_class = "cherrypy._cpnative_server.CPHTTPServer" - using_apache = False - using_wsgi = False - - def __str__(self): - return "Builtin HTTP Server on %s:%s" % (self.host, self.port) - - -class LocalWSGISupervisor(LocalSupervisor): - """Server supervisor for the builtin WSGI server.""" - - httpserver_class = "cherrypy._cpwsgi_server.CPWSGIServer" - using_apache = False - using_wsgi = True - - def __str__(self): - return "Builtin WSGI Server on %s:%s" % (self.host, self.port) - - def sync_apps(self): - """Hook a new WSGI app into the origin server.""" - cherrypy.server.httpserver.wsgi_app = self.get_app() - - def get_app(self, app=None): - """Obtain a new (decorated) WSGI app to hook into the origin server.""" - if app is None: - app = cherrypy.tree - - if self.conquer: - try: - import wsgiconq - except ImportError: - warnings.warn("Error importing wsgiconq. pyconquer will not run.") - else: - app = wsgiconq.WSGILogger(app, c_calls=True) - - if self.validate: - try: - from wsgiref import validate - except ImportError: - warnings.warn("Error importing wsgiref. The validator will not run.") - else: - #wraps the app in the validator - app = validate.validator(app) - - return app - - -def get_cpmodpy_supervisor(**options): - from cherrypy.test import modpy - sup = modpy.ModPythonSupervisor(**options) - sup.template = modpy.conf_cpmodpy - return sup - -def get_modpygw_supervisor(**options): - from cherrypy.test import modpy - sup = modpy.ModPythonSupervisor(**options) - sup.template = modpy.conf_modpython_gateway - sup.using_wsgi = True - return sup - -def get_modwsgi_supervisor(**options): - from cherrypy.test import modwsgi - return modwsgi.ModWSGISupervisor(**options) - -def get_modfcgid_supervisor(**options): - from cherrypy.test import modfcgid - return modfcgid.ModFCGISupervisor(**options) - -def get_modfastcgi_supervisor(**options): - from cherrypy.test import modfastcgi - return modfastcgi.ModFCGISupervisor(**options) - -def get_wsgi_u_supervisor(**options): - cherrypy.server.wsgi_version = ('u', 0) - return LocalWSGISupervisor(**options) - - -class CPWebCase(webtest.WebCase): - - script_name = "" - scheme = "http" - - available_servers = {'wsgi': LocalWSGISupervisor, - 'wsgi_u': get_wsgi_u_supervisor, - 'native': NativeServerSupervisor, - 'cpmodpy': get_cpmodpy_supervisor, - 'modpygw': get_modpygw_supervisor, - 'modwsgi': get_modwsgi_supervisor, - 'modfcgid': get_modfcgid_supervisor, - 'modfastcgi': get_modfastcgi_supervisor, - } - default_server = "wsgi" - - def _setup_server(cls, supervisor, conf): - v = sys.version.split()[0] - log.info("Python version used to run this test script: %s" % v) - log.info("CherryPy version: %s" % cherrypy.__version__) - if supervisor.scheme == "https": - ssl = " (ssl)" - else: - ssl = "" - log.info("HTTP server version: %s%s" % (supervisor.protocol, ssl)) - log.info("PID: %s" % os.getpid()) - - cherrypy.server.using_apache = supervisor.using_apache - cherrypy.server.using_wsgi = supervisor.using_wsgi - - if sys.platform[:4] == 'java': - cherrypy.config.update({'server.nodelay': False}) - - if isinstance(conf, basestring): - parser = cherrypy.lib.reprconf.Parser() - conf = parser.dict_from_file(conf).get('global', {}) - else: - conf = conf or {} - baseconf = conf.copy() - baseconf.update({'server.socket_host': supervisor.host, - 'server.socket_port': supervisor.port, - 'server.protocol_version': supervisor.protocol, - 'environment': "test_suite", - }) - if supervisor.scheme == "https": - #baseconf['server.ssl_module'] = 'builtin' - baseconf['server.ssl_certificate'] = serverpem - baseconf['server.ssl_private_key'] = serverpem - - # helper must be imported lazily so the coverage tool - # can run against module-level statements within cherrypy. - # Also, we have to do "from cherrypy.test import helper", - # exactly like each test module does, because a relative import - # would stick a second instance of webtest in sys.modules, - # and we wouldn't be able to globally override the port anymore. - if supervisor.scheme == "https": - webtest.WebCase.HTTP_CONN = HTTPSConnection - return baseconf - _setup_server = classmethod(_setup_server) - - def setup_class(cls): - '' - #Creates a server - conf = get_tst_config() - supervisor_factory = cls.available_servers.get(conf.get('server', 'wsgi')) - if supervisor_factory is None: - raise RuntimeError('Unknown server in config: %s' % conf['server']) - supervisor = supervisor_factory(**conf) - - #Copied from "run_test_suite" - cherrypy.config.reset() - baseconf = cls._setup_server(supervisor, conf) - cherrypy.config.update(baseconf) - setup_client() - - if hasattr(cls, 'setup_server'): - # Clear the cherrypy tree and clear the wsgi server so that - # it can be updated with the new root - cherrypy.tree = cherrypy._cptree.Tree() - cherrypy.server.httpserver = None - cls.setup_server() - # Add a resource for verifying there are no refleaks - # to *every* test class. - cherrypy.tree.mount(gctools.GCRoot(), '/gc') - cls.do_gc_test = True - supervisor.start(cls.__module__) - - cls.supervisor = supervisor - setup_class = classmethod(setup_class) - - def teardown_class(cls): - '' - if hasattr(cls, 'setup_server'): - cls.supervisor.stop() - teardown_class = classmethod(teardown_class) - - do_gc_test = False - - def test_gc(self): - if self.do_gc_test: - self.getPage("/gc/stats") - self.assertBody("Statistics:") - # Tell nose to run this last in each class. - # Prefer sys.maxint for Python 2.3, which didn't have float('inf') - test_gc.compat_co_firstlineno = getattr(sys, 'maxint', None) or float('inf') - - def prefix(self): - return self.script_name.rstrip("/") - - def base(self): - if ((self.scheme == "http" and self.PORT == 80) or - (self.scheme == "https" and self.PORT == 443)): - port = "" - else: - port = ":%s" % self.PORT - - return "%s://%s%s%s" % (self.scheme, self.HOST, port, - self.script_name.rstrip("/")) - - def exit(self): - sys.exit() - - def getPage(self, url, headers=None, method="GET", body=None, protocol=None): - """Open the url. Return status, headers, body.""" - if self.script_name: - url = httputil.urljoin(self.script_name, url) - return webtest.WebCase.getPage(self, url, headers, method, body, protocol) - - def skip(self, msg='skipped '): - raise nose.SkipTest(msg) - - def assertErrorPage(self, status, message=None, pattern=''): - """Compare the response body with a built in error page. - - The function will optionally look for the regexp pattern, - within the exception embedded in the error page.""" - - # This will never contain a traceback - page = cherrypy._cperror.get_error_page(status, message=message) - - # First, test the response body without checking the traceback. - # Stick a match-all group (.*) in to grab the traceback. - esc = re.escape - epage = esc(page) - epage = epage.replace(esc('
'),
-                              esc('
') + '(.*)' + esc('
')) - m = re.match(ntob(epage, self.encoding), self.body, re.DOTALL) - if not m: - self._handlewebError('Error page does not match; expected:\n' + page) - return - - # Now test the pattern against the traceback - if pattern is None: - # Special-case None to mean that there should be *no* traceback. - if m and m.group(1): - self._handlewebError('Error page contains traceback') - else: - if (m is None) or ( - not re.search(ntob(re.escape(pattern), self.encoding), - m.group(1))): - msg = 'Error page does not contain %s in traceback' - self._handlewebError(msg % repr(pattern)) - - date_tolerance = 2 - - def assertEqualDates(self, dt1, dt2, seconds=None): - """Assert abs(dt1 - dt2) is within Y seconds.""" - if seconds is None: - seconds = self.date_tolerance - - if dt1 > dt2: - diff = dt1 - dt2 - else: - diff = dt2 - dt1 - if not diff < datetime.timedelta(seconds=seconds): - raise AssertionError('%r and %r are not within %r seconds.' % - (dt1, dt2, seconds)) - - -def setup_client(): - """Set up the WebCase classes to match the server's socket settings.""" - webtest.WebCase.PORT = cherrypy.server.socket_port - webtest.WebCase.HOST = cherrypy.server.socket_host - if cherrypy.server.ssl_certificate: - CPWebCase.scheme = 'https' - -# --------------------------- Spawning helpers --------------------------- # - - -class CPProcess(object): - - pid_file = os.path.join(thisdir, 'test.pid') - config_file = os.path.join(thisdir, 'test.conf') - config_template = """[global] -server.socket_host: '%(host)s' -server.socket_port: %(port)s -checker.on: False -log.screen: False -log.error_file: r'%(error_log)s' -log.access_file: r'%(access_log)s' -%(ssl)s -%(extra)s -""" - error_log = os.path.join(thisdir, 'test.error.log') - access_log = os.path.join(thisdir, 'test.access.log') - - def __init__(self, wait=False, daemonize=False, ssl=False, socket_host=None, socket_port=None): - self.wait = wait - self.daemonize = daemonize - self.ssl = ssl - self.host = socket_host or cherrypy.server.socket_host - self.port = socket_port or cherrypy.server.socket_port - - def write_conf(self, extra=""): - if self.ssl: - serverpem = os.path.join(thisdir, 'test.pem') - ssl = """ -server.ssl_certificate: r'%s' -server.ssl_private_key: r'%s' -""" % (serverpem, serverpem) - else: - ssl = "" - - conf = self.config_template % { - 'host': self.host, - 'port': self.port, - 'error_log': self.error_log, - 'access_log': self.access_log, - 'ssl': ssl, - 'extra': extra, - } - f = open(self.config_file, 'wb') - f.write(ntob(conf, 'utf-8')) - f.close() - - def start(self, imports=None): - """Start cherryd in a subprocess.""" - cherrypy._cpserver.wait_for_free_port(self.host, self.port) - - args = [sys.executable, os.path.join(thisdir, '..', 'cherryd'), - '-c', self.config_file, '-p', self.pid_file] - - if not isinstance(imports, (list, tuple)): - imports = [imports] - for i in imports: - if i: - args.append('-i') - args.append(i) - - if self.daemonize: - args.append('-d') - - env = os.environ.copy() - # Make sure we import the cherrypy package in which this module is defined. - grandparentdir = os.path.abspath(os.path.join(thisdir, '..', '..')) - if env.get('PYTHONPATH', ''): - env['PYTHONPATH'] = os.pathsep.join((grandparentdir, env['PYTHONPATH'])) - else: - env['PYTHONPATH'] = grandparentdir - if self.wait: - self.exit_code = os.spawnve(os.P_WAIT, sys.executable, args, env) - else: - os.spawnve(os.P_NOWAIT, sys.executable, args, env) - cherrypy._cpserver.wait_for_occupied_port(self.host, self.port) - - # Give the engine a wee bit more time to finish STARTING - if self.daemonize: - time.sleep(2) - else: - time.sleep(1) - - def get_pid(self): - return int(open(self.pid_file, 'rb').read()) - - def join(self): - """Wait for the process to exit.""" - try: - try: - # Mac, UNIX - os.wait() - except AttributeError: - # Windows - try: - pid = self.get_pid() - except IOError: - # Assume the subprocess deleted the pidfile on shutdown. - pass - else: - os.waitpid(pid, 0) - except OSError: - x = sys.exc_info()[1] - if x.args != (10, 'No child processes'): - raise - diff --git a/libs/cherrypy/test/logtest.py b/libs/cherrypy/test/logtest.py deleted file mode 100644 index 3c6f114..0000000 --- a/libs/cherrypy/test/logtest.py +++ /dev/null @@ -1,188 +0,0 @@ -"""logtest, a unittest.TestCase helper for testing log output.""" - -import sys -import time - -import cherrypy -from cherrypy._cpcompat import basestring, ntob, unicodestr - - -try: - # On Windows, msvcrt.getch reads a single char without output. - import msvcrt - def getchar(): - return msvcrt.getch() -except ImportError: - # Unix getchr - import tty, termios - def getchar(): - fd = sys.stdin.fileno() - old_settings = termios.tcgetattr(fd) - try: - tty.setraw(sys.stdin.fileno()) - ch = sys.stdin.read(1) - finally: - termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) - return ch - - -class LogCase(object): - """unittest.TestCase mixin for testing log messages. - - logfile: a filename for the desired log. Yes, I know modes are evil, - but it makes the test functions so much cleaner to set this once. - - lastmarker: the last marker in the log. This can be used to search for - messages since the last marker. - - markerPrefix: a string with which to prefix log markers. This should be - unique enough from normal log output to use for marker identification. - """ - - logfile = None - lastmarker = None - markerPrefix = ntob("test suite marker: ") - - def _handleLogError(self, msg, data, marker, pattern): - print("") - print(" ERROR: %s" % msg) - - if not self.interactive: - raise self.failureException(msg) - - p = " Show: [L]og [M]arker [P]attern; [I]gnore, [R]aise, or sys.e[X]it >> " - sys.stdout.write(p + ' ') - # ARGH - sys.stdout.flush() - while True: - i = getchar().upper() - if i not in "MPLIRX": - continue - print(i.upper()) # Also prints new line - if i == "L": - for x, line in enumerate(data): - if (x + 1) % self.console_height == 0: - # The \r and comma should make the next line overwrite - sys.stdout.write("<-- More -->\r ") - m = getchar().lower() - # Erase our "More" prompt - sys.stdout.write(" \r ") - if m == "q": - break - print(line.rstrip()) - elif i == "M": - print(repr(marker or self.lastmarker)) - elif i == "P": - print(repr(pattern)) - elif i == "I": - # return without raising the normal exception - return - elif i == "R": - raise self.failureException(msg) - elif i == "X": - self.exit() - sys.stdout.write(p + ' ') - - def exit(self): - sys.exit() - - def emptyLog(self): - """Overwrite self.logfile with 0 bytes.""" - open(self.logfile, 'wb').write("") - - def markLog(self, key=None): - """Insert a marker line into the log and set self.lastmarker.""" - if key is None: - key = str(time.time()) - self.lastmarker = key - - open(self.logfile, 'ab+').write(ntob("%s%s\n" % (self.markerPrefix, key),"utf-8")) - - def _read_marked_region(self, marker=None): - """Return lines from self.logfile in the marked region. - - If marker is None, self.lastmarker is used. If the log hasn't - been marked (using self.markLog), the entire log will be returned. - """ -## # Give the logger time to finish writing? -## time.sleep(0.5) - - logfile = self.logfile - marker = marker or self.lastmarker - if marker is None: - return open(logfile, 'rb').readlines() - - if isinstance(marker, unicodestr): - marker = marker.encode('utf-8') - data = [] - in_region = False - for line in open(logfile, 'rb'): - if in_region: - if (line.startswith(self.markerPrefix) and not marker in line): - break - else: - data.append(line) - elif marker in line: - in_region = True - return data - - def assertInLog(self, line, marker=None): - """Fail if the given (partial) line is not in the log. - - The log will be searched from the given marker to the next marker. - If marker is None, self.lastmarker is used. If the log hasn't - been marked (using self.markLog), the entire log will be searched. - """ - data = self._read_marked_region(marker) - for logline in data: - if line in logline: - return - msg = "%r not found in log" % line - self._handleLogError(msg, data, marker, line) - - def assertNotInLog(self, line, marker=None): - """Fail if the given (partial) line is in the log. - - The log will be searched from the given marker to the next marker. - If marker is None, self.lastmarker is used. If the log hasn't - been marked (using self.markLog), the entire log will be searched. - """ - data = self._read_marked_region(marker) - for logline in data: - if line in logline: - msg = "%r found in log" % line - self._handleLogError(msg, data, marker, line) - - def assertLog(self, sliceargs, lines, marker=None): - """Fail if log.readlines()[sliceargs] is not contained in 'lines'. - - The log will be searched from the given marker to the next marker. - If marker is None, self.lastmarker is used. If the log hasn't - been marked (using self.markLog), the entire log will be searched. - """ - data = self._read_marked_region(marker) - if isinstance(sliceargs, int): - # Single arg. Use __getitem__ and allow lines to be str or list. - if isinstance(lines, (tuple, list)): - lines = lines[0] - if isinstance(lines, unicodestr): - lines = lines.encode('utf-8') - if lines not in data[sliceargs]: - msg = "%r not found on log line %r" % (lines, sliceargs) - self._handleLogError(msg, [data[sliceargs],"--EXTRA CONTEXT--"] + data[sliceargs+1:sliceargs+6], marker, lines) - else: - # Multiple args. Use __getslice__ and require lines to be list. - if isinstance(lines, tuple): - lines = list(lines) - elif isinstance(lines, basestring): - raise TypeError("The 'lines' arg must be a list when " - "'sliceargs' is a tuple.") - - start, stop = sliceargs - for line, logline in zip(lines, data[start:stop]): - if isinstance(line, unicodestr): - line = line.encode('utf-8') - if line not in logline: - msg = "%r not found in log" % line - self._handleLogError(msg, data[start:stop], marker, line) - diff --git a/libs/cherrypy/test/modfastcgi.py b/libs/cherrypy/test/modfastcgi.py deleted file mode 100644 index 95acf14..0000000 --- a/libs/cherrypy/test/modfastcgi.py +++ /dev/null @@ -1,135 +0,0 @@ -"""Wrapper for mod_fastcgi, for use as a CherryPy HTTP server when testing. - -To autostart fastcgi, the "apache" executable or script must be -on your system path, or you must override the global APACHE_PATH. -On some platforms, "apache" may be called "apachectl", "apache2ctl", -or "httpd"--create a symlink to them if needed. - -You'll also need the WSGIServer from flup.servers. -See http://projects.amor.org/misc/wiki/ModPythonGateway - - -KNOWN BUGS -========== - -1. Apache processes Range headers automatically; CherryPy's truncated - output is then truncated again by Apache. See test_core.testRanges. - This was worked around in http://www.cherrypy.org/changeset/1319. -2. Apache does not allow custom HTTP methods like CONNECT as per the spec. - See test_core.testHTTPMethods. -3. Max request header and body settings do not work with Apache. -4. Apache replaces status "reason phrases" automatically. For example, - CherryPy may set "304 Not modified" but Apache will write out - "304 Not Modified" (capital "M"). -5. Apache does not allow custom error codes as per the spec. -6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the - Request-URI too early. -7. mod_python will not read request bodies which use the "chunked" - transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block - instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and - mod_python's requestobject.c). -8. Apache will output a "Content-Length: 0" response header even if there's - no response entity body. This isn't really a bug; it just differs from - the CherryPy default. -""" - -import os -curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) -import re -import sys -import time - -import cherrypy -from cherrypy.process import plugins, servers -from cherrypy.test import helper - - -def read_process(cmd, args=""): - pipein, pipeout = os.popen4("%s %s" % (cmd, args)) - try: - firstline = pipeout.readline() - if (re.search(r"(not recognized|No such file|not found)", firstline, - re.IGNORECASE)): - raise IOError('%s must be on your system path.' % cmd) - output = firstline + pipeout.read() - finally: - pipeout.close() - return output - - -APACHE_PATH = "apache2ctl" -CONF_PATH = "fastcgi.conf" - -conf_fastcgi = """ -# Apache2 server conf file for testing CherryPy with mod_fastcgi. -# fumanchu: I had to hard-code paths due to crazy Debian layouts :( -ServerRoot /usr/lib/apache2 -User #1000 -ErrorLog %(root)s/mod_fastcgi.error.log - -DocumentRoot "%(root)s" -ServerName 127.0.0.1 -Listen %(port)s -LoadModule fastcgi_module modules/mod_fastcgi.so -LoadModule rewrite_module modules/mod_rewrite.so - -Options +ExecCGI -SetHandler fastcgi-script -RewriteEngine On -RewriteRule ^(.*)$ /fastcgi.pyc [L] -FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000 -""" - -def erase_script_name(environ, start_response): - environ['SCRIPT_NAME'] = '' - return cherrypy.tree(environ, start_response) - -class ModFCGISupervisor(helper.LocalWSGISupervisor): - - httpserver_class = "cherrypy.process.servers.FlupFCGIServer" - using_apache = True - using_wsgi = True - template = conf_fastcgi - - def __str__(self): - return "FCGI Server on %s:%s" % (self.host, self.port) - - def start(self, modulename): - cherrypy.server.httpserver = servers.FlupFCGIServer( - application=erase_script_name, bindAddress=('127.0.0.1', 4000)) - cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000) - cherrypy.server.socket_port = 4000 - # For FCGI, we both start apache... - self.start_apache() - # ...and our local server - cherrypy.engine.start() - self.sync_apps() - - def start_apache(self): - fcgiconf = CONF_PATH - if not os.path.isabs(fcgiconf): - fcgiconf = os.path.join(curdir, fcgiconf) - - # Write the Apache conf file. - f = open(fcgiconf, 'wb') - try: - server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1] - output = self.template % {'port': self.port, 'root': curdir, - 'server': server} - output = output.replace('\r\n', '\n') - f.write(output) - finally: - f.close() - - result = read_process(APACHE_PATH, "-k start -f %s" % fcgiconf) - if result: - print(result) - - def stop(self): - """Gracefully shutdown a server that is serving forever.""" - read_process(APACHE_PATH, "-k stop") - helper.LocalWSGISupervisor.stop(self) - - def sync_apps(self): - cherrypy.server.httpserver.fcgiserver.application = self.get_app(erase_script_name) - diff --git a/libs/cherrypy/test/modfcgid.py b/libs/cherrypy/test/modfcgid.py deleted file mode 100644 index 736aa4c..0000000 --- a/libs/cherrypy/test/modfcgid.py +++ /dev/null @@ -1,125 +0,0 @@ -"""Wrapper for mod_fcgid, for use as a CherryPy HTTP server when testing. - -To autostart fcgid, the "apache" executable or script must be -on your system path, or you must override the global APACHE_PATH. -On some platforms, "apache" may be called "apachectl", "apache2ctl", -or "httpd"--create a symlink to them if needed. - -You'll also need the WSGIServer from flup.servers. -See http://projects.amor.org/misc/wiki/ModPythonGateway - - -KNOWN BUGS -========== - -1. Apache processes Range headers automatically; CherryPy's truncated - output is then truncated again by Apache. See test_core.testRanges. - This was worked around in http://www.cherrypy.org/changeset/1319. -2. Apache does not allow custom HTTP methods like CONNECT as per the spec. - See test_core.testHTTPMethods. -3. Max request header and body settings do not work with Apache. -4. Apache replaces status "reason phrases" automatically. For example, - CherryPy may set "304 Not modified" but Apache will write out - "304 Not Modified" (capital "M"). -5. Apache does not allow custom error codes as per the spec. -6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the - Request-URI too early. -7. mod_python will not read request bodies which use the "chunked" - transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block - instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and - mod_python's requestobject.c). -8. Apache will output a "Content-Length: 0" response header even if there's - no response entity body. This isn't really a bug; it just differs from - the CherryPy default. -""" - -import os -curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) -import re -import sys -import time - -import cherrypy -from cherrypy._cpcompat import ntob -from cherrypy.process import plugins, servers -from cherrypy.test import helper - - -def read_process(cmd, args=""): - pipein, pipeout = os.popen4("%s %s" % (cmd, args)) - try: - firstline = pipeout.readline() - if (re.search(r"(not recognized|No such file|not found)", firstline, - re.IGNORECASE)): - raise IOError('%s must be on your system path.' % cmd) - output = firstline + pipeout.read() - finally: - pipeout.close() - return output - - -APACHE_PATH = "httpd" -CONF_PATH = "fcgi.conf" - -conf_fcgid = """ -# Apache2 server conf file for testing CherryPy with mod_fcgid. - -DocumentRoot "%(root)s" -ServerName 127.0.0.1 -Listen %(port)s -LoadModule fastcgi_module modules/mod_fastcgi.dll -LoadModule rewrite_module modules/mod_rewrite.so - -Options ExecCGI -SetHandler fastcgi-script -RewriteEngine On -RewriteRule ^(.*)$ /fastcgi.pyc [L] -FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000 -""" - -class ModFCGISupervisor(helper.LocalSupervisor): - - using_apache = True - using_wsgi = True - template = conf_fcgid - - def __str__(self): - return "FCGI Server on %s:%s" % (self.host, self.port) - - def start(self, modulename): - cherrypy.server.httpserver = servers.FlupFCGIServer( - application=cherrypy.tree, bindAddress=('127.0.0.1', 4000)) - cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000) - # For FCGI, we both start apache... - self.start_apache() - # ...and our local server - helper.LocalServer.start(self, modulename) - - def start_apache(self): - fcgiconf = CONF_PATH - if not os.path.isabs(fcgiconf): - fcgiconf = os.path.join(curdir, fcgiconf) - - # Write the Apache conf file. - f = open(fcgiconf, 'wb') - try: - server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1] - output = self.template % {'port': self.port, 'root': curdir, - 'server': server} - output = ntob(output.replace('\r\n', '\n')) - f.write(output) - finally: - f.close() - - result = read_process(APACHE_PATH, "-k start -f %s" % fcgiconf) - if result: - print(result) - - def stop(self): - """Gracefully shutdown a server that is serving forever.""" - read_process(APACHE_PATH, "-k stop") - helper.LocalServer.stop(self) - - def sync_apps(self): - cherrypy.server.httpserver.fcgiserver.application = self.get_app() - diff --git a/libs/cherrypy/test/modpy.py b/libs/cherrypy/test/modpy.py deleted file mode 100644 index 519571f..0000000 --- a/libs/cherrypy/test/modpy.py +++ /dev/null @@ -1,163 +0,0 @@ -"""Wrapper for mod_python, for use as a CherryPy HTTP server when testing. - -To autostart modpython, the "apache" executable or script must be -on your system path, or you must override the global APACHE_PATH. -On some platforms, "apache" may be called "apachectl" or "apache2ctl"-- -create a symlink to them if needed. - -If you wish to test the WSGI interface instead of our _cpmodpy interface, -you also need the 'modpython_gateway' module at: -http://projects.amor.org/misc/wiki/ModPythonGateway - - -KNOWN BUGS -========== - -1. Apache processes Range headers automatically; CherryPy's truncated - output is then truncated again by Apache. See test_core.testRanges. - This was worked around in http://www.cherrypy.org/changeset/1319. -2. Apache does not allow custom HTTP methods like CONNECT as per the spec. - See test_core.testHTTPMethods. -3. Max request header and body settings do not work with Apache. -4. Apache replaces status "reason phrases" automatically. For example, - CherryPy may set "304 Not modified" but Apache will write out - "304 Not Modified" (capital "M"). -5. Apache does not allow custom error codes as per the spec. -6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the - Request-URI too early. -7. mod_python will not read request bodies which use the "chunked" - transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block - instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and - mod_python's requestobject.c). -8. Apache will output a "Content-Length: 0" response header even if there's - no response entity body. This isn't really a bug; it just differs from - the CherryPy default. -""" - -import os -curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) -import re -import time - -from cherrypy.test import helper - - -def read_process(cmd, args=""): - pipein, pipeout = os.popen4("%s %s" % (cmd, args)) - try: - firstline = pipeout.readline() - if (re.search(r"(not recognized|No such file|not found)", firstline, - re.IGNORECASE)): - raise IOError('%s must be on your system path.' % cmd) - output = firstline + pipeout.read() - finally: - pipeout.close() - return output - - -APACHE_PATH = "httpd" -CONF_PATH = "test_mp.conf" - -conf_modpython_gateway = """ -# Apache2 server conf file for testing CherryPy with modpython_gateway. - -ServerName 127.0.0.1 -DocumentRoot "/" -Listen %(port)s -LoadModule python_module modules/mod_python.so - -SetHandler python-program -PythonFixupHandler cherrypy.test.modpy::wsgisetup -PythonOption testmod %(modulename)s -PythonHandler modpython_gateway::handler -PythonOption wsgi.application cherrypy::tree -PythonOption socket_host %(host)s -PythonDebug On -""" - -conf_cpmodpy = """ -# Apache2 server conf file for testing CherryPy with _cpmodpy. - -ServerName 127.0.0.1 -DocumentRoot "/" -Listen %(port)s -LoadModule python_module modules/mod_python.so - -SetHandler python-program -PythonFixupHandler cherrypy.test.modpy::cpmodpysetup -PythonHandler cherrypy._cpmodpy::handler -PythonOption cherrypy.setup cherrypy.test.%(modulename)s::setup_server -PythonOption socket_host %(host)s -PythonDebug On -""" - -class ModPythonSupervisor(helper.Supervisor): - - using_apache = True - using_wsgi = False - template = None - - def __str__(self): - return "ModPython Server on %s:%s" % (self.host, self.port) - - def start(self, modulename): - mpconf = CONF_PATH - if not os.path.isabs(mpconf): - mpconf = os.path.join(curdir, mpconf) - - f = open(mpconf, 'wb') - try: - f.write(self.template % - {'port': self.port, 'modulename': modulename, - 'host': self.host}) - finally: - f.close() - - result = read_process(APACHE_PATH, "-k start -f %s" % mpconf) - if result: - print(result) - - def stop(self): - """Gracefully shutdown a server that is serving forever.""" - read_process(APACHE_PATH, "-k stop") - - -loaded = False -def wsgisetup(req): - global loaded - if not loaded: - loaded = True - options = req.get_options() - - import cherrypy - cherrypy.config.update({ - "log.error_file": os.path.join(curdir, "test.log"), - "environment": "test_suite", - "server.socket_host": options['socket_host'], - }) - - modname = options['testmod'] - mod = __import__(modname, globals(), locals(), ['']) - mod.setup_server() - - cherrypy.server.unsubscribe() - cherrypy.engine.start() - from mod_python import apache - return apache.OK - - -def cpmodpysetup(req): - global loaded - if not loaded: - loaded = True - options = req.get_options() - - import cherrypy - cherrypy.config.update({ - "log.error_file": os.path.join(curdir, "test.log"), - "environment": "test_suite", - "server.socket_host": options['socket_host'], - }) - from mod_python import apache - return apache.OK - diff --git a/libs/cherrypy/test/modwsgi.py b/libs/cherrypy/test/modwsgi.py deleted file mode 100644 index 309a541..0000000 --- a/libs/cherrypy/test/modwsgi.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Wrapper for mod_wsgi, for use as a CherryPy HTTP server. - -To autostart modwsgi, the "apache" executable or script must be -on your system path, or you must override the global APACHE_PATH. -On some platforms, "apache" may be called "apachectl" or "apache2ctl"-- -create a symlink to them if needed. - - -KNOWN BUGS -========== - -##1. Apache processes Range headers automatically; CherryPy's truncated -## output is then truncated again by Apache. See test_core.testRanges. -## This was worked around in http://www.cherrypy.org/changeset/1319. -2. Apache does not allow custom HTTP methods like CONNECT as per the spec. - See test_core.testHTTPMethods. -3. Max request header and body settings do not work with Apache. -##4. Apache replaces status "reason phrases" automatically. For example, -## CherryPy may set "304 Not modified" but Apache will write out -## "304 Not Modified" (capital "M"). -##5. Apache does not allow custom error codes as per the spec. -##6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the -## Request-URI too early. -7. mod_wsgi will not read request bodies which use the "chunked" - transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block - instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and - mod_python's requestobject.c). -8. When responding with 204 No Content, mod_wsgi adds a Content-Length - header for you. -9. When an error is raised, mod_wsgi has no facility for printing a - traceback as the response content (it's sent to the Apache log instead). -10. Startup and shutdown of Apache when running mod_wsgi seems slow. -""" - -import os -curdir = os.path.abspath(os.path.dirname(__file__)) -import re -import sys -import time - -import cherrypy -from cherrypy.test import helper, webtest - - -def read_process(cmd, args=""): - pipein, pipeout = os.popen4("%s %s" % (cmd, args)) - try: - firstline = pipeout.readline() - if (re.search(r"(not recognized|No such file|not found)", firstline, - re.IGNORECASE)): - raise IOError('%s must be on your system path.' % cmd) - output = firstline + pipeout.read() - finally: - pipeout.close() - return output - - -if sys.platform == 'win32': - APACHE_PATH = "httpd" -else: - APACHE_PATH = "apache" - -CONF_PATH = "test_mw.conf" - -conf_modwsgi = r""" -# Apache2 server conf file for testing CherryPy with modpython_gateway. - -ServerName 127.0.0.1 -DocumentRoot "/" -Listen %(port)s - -AllowEncodedSlashes On -LoadModule rewrite_module modules/mod_rewrite.so -RewriteEngine on -RewriteMap escaping int:escape - -LoadModule log_config_module modules/mod_log_config.so -LogFormat "%%h %%l %%u %%t \"%%r\" %%>s %%b \"%%{Referer}i\" \"%%{User-agent}i\"" combined -CustomLog "%(curdir)s/apache.access.log" combined -ErrorLog "%(curdir)s/apache.error.log" -LogLevel debug - -LoadModule wsgi_module modules/mod_wsgi.so -LoadModule env_module modules/mod_env.so - -WSGIScriptAlias / "%(curdir)s/modwsgi.py" -SetEnv testmod %(testmod)s -""" - - -class ModWSGISupervisor(helper.Supervisor): - """Server Controller for ModWSGI and CherryPy.""" - - using_apache = True - using_wsgi = True - template=conf_modwsgi - - def __str__(self): - return "ModWSGI Server on %s:%s" % (self.host, self.port) - - def start(self, modulename): - mpconf = CONF_PATH - if not os.path.isabs(mpconf): - mpconf = os.path.join(curdir, mpconf) - - f = open(mpconf, 'wb') - try: - output = (self.template % - {'port': self.port, 'testmod': modulename, - 'curdir': curdir}) - f.write(output) - finally: - f.close() - - result = read_process(APACHE_PATH, "-k start -f %s" % mpconf) - if result: - print(result) - - # Make a request so mod_wsgi starts up our app. - # If we don't, concurrent initial requests will 404. - cherrypy._cpserver.wait_for_occupied_port("127.0.0.1", self.port) - webtest.openURL('/ihopetheresnodefault', port=self.port) - time.sleep(1) - - def stop(self): - """Gracefully shutdown a server that is serving forever.""" - read_process(APACHE_PATH, "-k stop") - - -loaded = False -def application(environ, start_response): - import cherrypy - global loaded - if not loaded: - loaded = True - modname = "cherrypy.test." + environ['testmod'] - mod = __import__(modname, globals(), locals(), ['']) - mod.setup_server() - - cherrypy.config.update({ - "log.error_file": os.path.join(curdir, "test.error.log"), - "log.access_file": os.path.join(curdir, "test.access.log"), - "environment": "test_suite", - "engine.SIGHUP": None, - "engine.SIGTERM": None, - }) - return cherrypy.tree(environ, start_response) - diff --git a/libs/cherrypy/test/sessiondemo.py b/libs/cherrypy/test/sessiondemo.py deleted file mode 100644 index 342e5b5..0000000 --- a/libs/cherrypy/test/sessiondemo.py +++ /dev/null @@ -1,153 +0,0 @@ -#!/usr/bin/python -"""A session demonstration app.""" - -import calendar -from datetime import datetime -import sys -import cherrypy -from cherrypy.lib import sessions -from cherrypy._cpcompat import copyitems - - -page = """ - - - - - - - -

Session Demo

-

Reload this page. The session ID should not change from one reload to the next

-

Index | Expire | Regenerate

- - - - - - - - - -
Session ID:%(sessionid)s

%(changemsg)s

Request Cookie%(reqcookie)s
Response Cookie%(respcookie)s

Session Data%(sessiondata)s
Server Time%(servertime)s (Unix time: %(serverunixtime)s)
Browser Time 
Cherrypy Version:%(cpversion)s
Python Version:%(pyversion)s
- -""" - -class Root(object): - - def page(self): - changemsg = [] - if cherrypy.session.id != cherrypy.session.originalid: - if cherrypy.session.originalid is None: - changemsg.append('Created new session because no session id was given.') - if cherrypy.session.missing: - changemsg.append('Created new session due to missing (expired or malicious) session.') - if cherrypy.session.regenerated: - changemsg.append('Application generated a new session.') - - try: - expires = cherrypy.response.cookie['session_id']['expires'] - except KeyError: - expires = '' - - return page % { - 'sessionid': cherrypy.session.id, - 'changemsg': '
'.join(changemsg), - 'respcookie': cherrypy.response.cookie.output(), - 'reqcookie': cherrypy.request.cookie.output(), - 'sessiondata': copyitems(cherrypy.session), - 'servertime': datetime.utcnow().strftime("%Y/%m/%d %H:%M") + " UTC", - 'serverunixtime': calendar.timegm(datetime.utcnow().timetuple()), - 'cpversion': cherrypy.__version__, - 'pyversion': sys.version, - 'expires': expires, - } - - def index(self): - # Must modify data or the session will not be saved. - cherrypy.session['color'] = 'green' - return self.page() - index.exposed = True - - def expire(self): - sessions.expire() - return self.page() - expire.exposed = True - - def regen(self): - cherrypy.session.regenerate() - # Must modify data or the session will not be saved. - cherrypy.session['color'] = 'yellow' - return self.page() - regen.exposed = True - -if __name__ == '__main__': - cherrypy.config.update({ - #'environment': 'production', - 'log.screen': True, - 'tools.sessions.on': True, - }) - cherrypy.quickstart(Root()) - diff --git a/libs/cherrypy/test/static/dirback.jpg b/libs/cherrypy/test/static/dirback.jpg deleted file mode 100644 index 530e6d6a386fc097f3a1dbabbde2d80fec1175ac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18238 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�H;co1wWja&aY$n> z=Jt*bcyA#%hhcNqg668+&REt|@npB)Tns;=irh|HHDMT^HWE z#C$boDf4Wp?=c*f{G|rh8Ioq4D|YQ=dfFnlQvJ-`<-5)X^Uo-Y;(1l2Yn4*H<|~6K z-)Yv*>Wj`AH1MPxX55$d;&P9#wA%Nk9nPyH58q!E#vdl$LqWtF_L+4XMD z<`>%@IIkv%g^ORQb-`?yf6?-Sm$L9{tgD z`k9^E#p_?gKD6vyvg72GscOFuri))ZcHBO6X%fq6pE-Sd=5)1~9&AtioGo7L|6qym zX0b;LU6R=H6Mt)0=zgC%vr@aFf6BXV`%XjGW67x*ALSMAiscJ^peX%%j0+cL3whIiM} zHTTcVKeBb5(L;5vnaK$pn`+(k$~2ho|8&{;z3u#Wz2a!KTY3jB8YsQI7OU5|xlU*X zliOvFuin0E`6uYj@<|JF=lxV(I`xv)3?9%J?qtb%ukw?);-*}=dirYUP5F>^2+`5N%)27^VE=O3@H4k{i~jjr7~wMxWv`;CO_ zw!x;2ODqg0*$XlDOx`3sD@Wx`Ytm)*CYB=~rle1r9dY16L8nW@7GFcN0Mob_mo4#K hs~Z9j+;LCQxz+hB)cnY~*Aq-`s;892Y`FaYCIBsyhfx3k diff --git a/libs/cherrypy/test/static/index.html b/libs/cherrypy/test/static/index.html deleted file mode 100644 index b9f5f09..0000000 --- a/libs/cherrypy/test/static/index.html +++ /dev/null @@ -1 +0,0 @@ -Hello, world diff --git a/libs/cherrypy/test/style.css b/libs/cherrypy/test/style.css deleted file mode 100644 index b266e93..0000000 --- a/libs/cherrypy/test/style.css +++ /dev/null @@ -1 +0,0 @@ -Dummy stylesheet diff --git a/libs/cherrypy/test/test.pem b/libs/cherrypy/test/test.pem deleted file mode 100644 index 47a4704..0000000 --- a/libs/cherrypy/test/test.pem +++ /dev/null @@ -1,38 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQDBKo554mzIMY+AByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZ -R9L4WtImEew05FY3Izerfm3MN3+MC0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Kn -da+O6xldVSosu8Ev3z9VZ94iC/ZgKzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQAB -AoGAWOCF0ZrWxn3XMucWq2LNwPKqlvVGwbIwX3cDmX22zmnM4Fy6arXbYh4XlyCj -9+ofqRrxIFz5k/7tFriTmZ0xag5+Jdx+Kwg0/twiP7XCNKipFogwe1Hznw8OFAoT -enKBdj2+/n2o0Bvo/tDB59m9L/538d46JGQUmJlzMyqYikECQQDyoq+8CtMNvE18 -8VgHcR/KtApxWAjj4HpaHYL637ATjThetUZkW92mgDgowyplthusxdNqhHWyv7E8 -tWNdYErZAkEAy85ShTR0M5aWmrE7o0r0SpWInAkNBH9aXQRRARFYsdBtNfRu6I0i -0lvU9wiu3eF57FMEC86yViZ5UBnQfTu7vQJAVesj/Zt7pwaCDfdMa740OsxMUlyR -MVhhGx4OLpYdPJ8qUecxGQKq13XZ7R1HGyNEY4bd2X80Smq08UFuATfC6QJAH8UB -yBHtKz2GLIcELOg6PIYizW/7v3+6rlVF60yw7sb2vzpjL40QqIn4IKoR2DSVtOkb -8FtAIX3N21aq0VrGYQJBAIPiaEc2AZ8Bq2GC4F3wOz/BxJ/izvnkiotR12QK4fh5 -yjZMhTjWCas5zwHR5PDjlD88AWGDMsZ1PicD4348xJQ= ------END RSA PRIVATE KEY----- ------BEGIN CERTIFICATE----- -MIIDxTCCAy6gAwIBAgIJAI18BD7eQxlGMA0GCSqGSIb3DQEBBAUAMIGeMQswCQYD -VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU2FuIERpZWdv -MRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0MREwDwYDVQQLEwhkZXYtdGVzdDEW -MBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4GCSqGSIb3DQEJARYRcmVtaUBjaGVy -cnlweS5vcmcwHhcNMDYwOTA5MTkyMDIwWhcNMzQwMTI0MTkyMDIwWjCBnjELMAkG -A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVNhbiBEaWVn -bzEZMBcGA1UEChMQQ2hlcnJ5UHkgUHJvamVjdDERMA8GA1UECxMIZGV2LXRlc3Qx -FjAUBgNVBAMTDUNoZXJyeVB5IFRlYW0xIDAeBgkqhkiG9w0BCQEWEXJlbWlAY2hl -cnJ5cHkub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBKo554mzIMY+A -ByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZR9L4WtImEew05FY3Izerfm3MN3+M -C0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Knda+O6xldVSosu8Ev3z9VZ94iC/Zg -KzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQABo4IBBzCCAQMwHQYDVR0OBBYEFDIQ -2feb71tVZCWpU0qJ/Tw+wdtoMIHTBgNVHSMEgcswgciAFDIQ2feb71tVZCWpU0qJ -/Tw+wdtooYGkpIGhMIGeMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p -YTESMBAGA1UEBxMJU2FuIERpZWdvMRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0 -MREwDwYDVQQLEwhkZXYtdGVzdDEWMBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4G -CSqGSIb3DQEJARYRcmVtaUBjaGVycnlweS5vcmeCCQCNfAQ+3kMZRjAMBgNVHRME -BTADAQH/MA0GCSqGSIb3DQEBBAUAA4GBAL7AAQz7IePV48ZTAFHKr88ntPALsL5S -8vHCZPNMevNkLTj3DYUw2BcnENxMjm1kou2F2BkvheBPNZKIhc6z4hAml3ed1xa2 -D7w6e6OTcstdK/+KrPDDHeOP1dhMWNs2JE1bNlfF1LiXzYKSXpe88eCKjCXsCT/T -NluCaWQys3MS ------END CERTIFICATE----- diff --git a/libs/cherrypy/test/test_auth_basic.py b/libs/cherrypy/test/test_auth_basic.py deleted file mode 100644 index 3a9781d..0000000 --- a/libs/cherrypy/test/test_auth_basic.py +++ /dev/null @@ -1,79 +0,0 @@ -# This file is part of CherryPy -# -*- coding: utf-8 -*- -# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 - -import cherrypy -from cherrypy._cpcompat import md5, ntob -from cherrypy.lib import auth_basic -from cherrypy.test import helper - - -class BasicAuthTest(helper.CPWebCase): - - def setup_server(): - class Root: - def index(self): - return "This is public." - index.exposed = True - - class BasicProtected: - def index(self): - return "Hello %s, you've been authorized." % cherrypy.request.login - index.exposed = True - - class BasicProtected2: - def index(self): - return "Hello %s, you've been authorized." % cherrypy.request.login - index.exposed = True - - userpassdict = {'xuser' : 'xpassword'} - userhashdict = {'xuser' : md5(ntob('xpassword')).hexdigest()} - - def checkpasshash(realm, user, password): - p = userhashdict.get(user) - return p and p == md5(ntob(password)).hexdigest() or False - - conf = {'/basic': {'tools.auth_basic.on': True, - 'tools.auth_basic.realm': 'wonderland', - 'tools.auth_basic.checkpassword': auth_basic.checkpassword_dict(userpassdict)}, - '/basic2': {'tools.auth_basic.on': True, - 'tools.auth_basic.realm': 'wonderland', - 'tools.auth_basic.checkpassword': checkpasshash}, - } - - root = Root() - root.basic = BasicProtected() - root.basic2 = BasicProtected2() - cherrypy.tree.mount(root, config=conf) - setup_server = staticmethod(setup_server) - - def testPublic(self): - self.getPage("/") - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/html;charset=utf-8') - self.assertBody('This is public.') - - def testBasic(self): - self.getPage("/basic/") - self.assertStatus(401) - self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"') - - self.getPage('/basic/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) - self.assertStatus(401) - - self.getPage('/basic/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')]) - self.assertStatus('200 OK') - self.assertBody("Hello xuser, you've been authorized.") - - def testBasic2(self): - self.getPage("/basic2/") - self.assertStatus(401) - self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"') - - self.getPage('/basic2/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) - self.assertStatus(401) - - self.getPage('/basic2/', [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')]) - self.assertStatus('200 OK') - self.assertBody("Hello xuser, you've been authorized.") - diff --git a/libs/cherrypy/test/test_auth_digest.py b/libs/cherrypy/test/test_auth_digest.py deleted file mode 100644 index 1960fa8..0000000 --- a/libs/cherrypy/test/test_auth_digest.py +++ /dev/null @@ -1,115 +0,0 @@ -# This file is part of CherryPy -# -*- coding: utf-8 -*- -# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 - - -import cherrypy -from cherrypy.lib import auth_digest - -from cherrypy.test import helper - -class DigestAuthTest(helper.CPWebCase): - - def setup_server(): - class Root: - def index(self): - return "This is public." - index.exposed = True - - class DigestProtected: - def index(self): - return "Hello %s, you've been authorized." % cherrypy.request.login - index.exposed = True - - def fetch_users(): - return {'test': 'test'} - - - get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(fetch_users()) - conf = {'/digest': {'tools.auth_digest.on': True, - 'tools.auth_digest.realm': 'localhost', - 'tools.auth_digest.get_ha1': get_ha1, - 'tools.auth_digest.key': 'a565c27146791cfb', - 'tools.auth_digest.debug': 'True'}} - - root = Root() - root.digest = DigestProtected() - cherrypy.tree.mount(root, config=conf) - setup_server = staticmethod(setup_server) - - def testPublic(self): - self.getPage("/") - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/html;charset=utf-8') - self.assertBody('This is public.') - - def testDigest(self): - self.getPage("/digest/") - self.assertStatus(401) - - value = None - for k, v in self.headers: - if k.lower() == "www-authenticate": - if v.startswith("Digest"): - value = v - break - - if value is None: - self._handlewebError("Digest authentification scheme was not found") - - value = value[7:] - items = value.split(', ') - tokens = {} - for item in items: - key, value = item.split('=') - tokens[key.lower()] = value - - missing_msg = "%s is missing" - bad_value_msg = "'%s' was expecting '%s' but found '%s'" - nonce = None - if 'realm' not in tokens: - self._handlewebError(missing_msg % 'realm') - elif tokens['realm'] != '"localhost"': - self._handlewebError(bad_value_msg % ('realm', '"localhost"', tokens['realm'])) - if 'nonce' not in tokens: - self._handlewebError(missing_msg % 'nonce') - else: - nonce = tokens['nonce'].strip('"') - if 'algorithm' not in tokens: - self._handlewebError(missing_msg % 'algorithm') - elif tokens['algorithm'] != '"MD5"': - self._handlewebError(bad_value_msg % ('algorithm', '"MD5"', tokens['algorithm'])) - if 'qop' not in tokens: - self._handlewebError(missing_msg % 'qop') - elif tokens['qop'] != '"auth"': - self._handlewebError(bad_value_msg % ('qop', '"auth"', tokens['qop'])) - - get_ha1 = auth_digest.get_ha1_dict_plain({'test' : 'test'}) - - # Test user agent response with a wrong value for 'realm' - base_auth = 'Digest username="test", realm="wrong realm", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"' - - auth_header = base_auth % (nonce, '11111111111111111111111111111111', '00000001') - auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET') - # calculate the response digest - ha1 = get_ha1(auth.realm, 'test') - response = auth.request_digest(ha1) - # send response with correct response digest, but wrong realm - auth_header = base_auth % (nonce, response, '00000001') - self.getPage('/digest/', [('Authorization', auth_header)]) - self.assertStatus(401) - - # Test that must pass - base_auth = 'Digest username="test", realm="localhost", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"' - - auth_header = base_auth % (nonce, '11111111111111111111111111111111', '00000001') - auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET') - # calculate the response digest - ha1 = get_ha1('localhost', 'test') - response = auth.request_digest(ha1) - # send response with correct response digest - auth_header = base_auth % (nonce, response, '00000001') - self.getPage('/digest/', [('Authorization', auth_header)]) - self.assertStatus('200 OK') - self.assertBody("Hello test, you've been authorized.") - diff --git a/libs/cherrypy/test/test_bus.py b/libs/cherrypy/test/test_bus.py deleted file mode 100644 index 51c1022..0000000 --- a/libs/cherrypy/test/test_bus.py +++ /dev/null @@ -1,263 +0,0 @@ -import threading -import time -import unittest - -import cherrypy -from cherrypy._cpcompat import get_daemon, set -from cherrypy.process import wspbus - - -msg = "Listener %d on channel %s: %s." - - -class PublishSubscribeTests(unittest.TestCase): - - def get_listener(self, channel, index): - def listener(arg=None): - self.responses.append(msg % (index, channel, arg)) - return listener - - def test_builtin_channels(self): - b = wspbus.Bus() - - self.responses, expected = [], [] - - for channel in b.listeners: - for index, priority in enumerate([100, 50, 0, 51]): - b.subscribe(channel, self.get_listener(channel, index), priority) - - for channel in b.listeners: - b.publish(channel) - expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)]) - b.publish(channel, arg=79347) - expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)]) - - self.assertEqual(self.responses, expected) - - def test_custom_channels(self): - b = wspbus.Bus() - - self.responses, expected = [], [] - - custom_listeners = ('hugh', 'louis', 'dewey') - for channel in custom_listeners: - for index, priority in enumerate([None, 10, 60, 40]): - b.subscribe(channel, self.get_listener(channel, index), priority) - - for channel in custom_listeners: - b.publish(channel, 'ah so') - expected.extend([msg % (i, channel, 'ah so') for i in (1, 3, 0, 2)]) - b.publish(channel) - expected.extend([msg % (i, channel, None) for i in (1, 3, 0, 2)]) - - self.assertEqual(self.responses, expected) - - def test_listener_errors(self): - b = wspbus.Bus() - - self.responses, expected = [], [] - channels = [c for c in b.listeners if c != 'log'] - - for channel in channels: - b.subscribe(channel, self.get_listener(channel, 1)) - # This will break since the lambda takes no args. - b.subscribe(channel, lambda: None, priority=20) - - for channel in channels: - self.assertRaises(wspbus.ChannelFailures, b.publish, channel, 123) - expected.append(msg % (1, channel, 123)) - - self.assertEqual(self.responses, expected) - - -class BusMethodTests(unittest.TestCase): - - def log(self, bus): - self._log_entries = [] - def logit(msg, level): - self._log_entries.append(msg) - bus.subscribe('log', logit) - - def assertLog(self, entries): - self.assertEqual(self._log_entries, entries) - - def get_listener(self, channel, index): - def listener(arg=None): - self.responses.append(msg % (index, channel, arg)) - return listener - - def test_start(self): - b = wspbus.Bus() - self.log(b) - - self.responses = [] - num = 3 - for index in range(num): - b.subscribe('start', self.get_listener('start', index)) - - b.start() - try: - # The start method MUST call all 'start' listeners. - self.assertEqual(set(self.responses), - set([msg % (i, 'start', None) for i in range(num)])) - # The start method MUST move the state to STARTED - # (or EXITING, if errors occur) - self.assertEqual(b.state, b.states.STARTED) - # The start method MUST log its states. - self.assertLog(['Bus STARTING', 'Bus STARTED']) - finally: - # Exit so the atexit handler doesn't complain. - b.exit() - - def test_stop(self): - b = wspbus.Bus() - self.log(b) - - self.responses = [] - num = 3 - for index in range(num): - b.subscribe('stop', self.get_listener('stop', index)) - - b.stop() - - # The stop method MUST call all 'stop' listeners. - self.assertEqual(set(self.responses), - set([msg % (i, 'stop', None) for i in range(num)])) - # The stop method MUST move the state to STOPPED - self.assertEqual(b.state, b.states.STOPPED) - # The stop method MUST log its states. - self.assertLog(['Bus STOPPING', 'Bus STOPPED']) - - def test_graceful(self): - b = wspbus.Bus() - self.log(b) - - self.responses = [] - num = 3 - for index in range(num): - b.subscribe('graceful', self.get_listener('graceful', index)) - - b.graceful() - - # The graceful method MUST call all 'graceful' listeners. - self.assertEqual(set(self.responses), - set([msg % (i, 'graceful', None) for i in range(num)])) - # The graceful method MUST log its states. - self.assertLog(['Bus graceful']) - - def test_exit(self): - b = wspbus.Bus() - self.log(b) - - self.responses = [] - num = 3 - for index in range(num): - b.subscribe('stop', self.get_listener('stop', index)) - b.subscribe('exit', self.get_listener('exit', index)) - - b.exit() - - # The exit method MUST call all 'stop' listeners, - # and then all 'exit' listeners. - self.assertEqual(set(self.responses), - set([msg % (i, 'stop', None) for i in range(num)] + - [msg % (i, 'exit', None) for i in range(num)])) - # The exit method MUST move the state to EXITING - self.assertEqual(b.state, b.states.EXITING) - # The exit method MUST log its states. - self.assertLog(['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED']) - - def test_wait(self): - b = wspbus.Bus() - - def f(method): - time.sleep(0.2) - getattr(b, method)() - - for method, states in [('start', [b.states.STARTED]), - ('stop', [b.states.STOPPED]), - ('start', [b.states.STARTING, b.states.STARTED]), - ('exit', [b.states.EXITING]), - ]: - threading.Thread(target=f, args=(method,)).start() - b.wait(states) - - # The wait method MUST wait for the given state(s). - if b.state not in states: - self.fail("State %r not in %r" % (b.state, states)) - - def test_block(self): - b = wspbus.Bus() - self.log(b) - - def f(): - time.sleep(0.2) - b.exit() - def g(): - time.sleep(0.4) - threading.Thread(target=f).start() - threading.Thread(target=g).start() - threads = [t for t in threading.enumerate() if not get_daemon(t)] - self.assertEqual(len(threads), 3) - - b.block() - - # The block method MUST wait for the EXITING state. - self.assertEqual(b.state, b.states.EXITING) - # The block method MUST wait for ALL non-main, non-daemon threads to finish. - threads = [t for t in threading.enumerate() if not get_daemon(t)] - self.assertEqual(len(threads), 1) - # The last message will mention an indeterminable thread name; ignore it - self.assertEqual(self._log_entries[:-1], - ['Bus STOPPING', 'Bus STOPPED', - 'Bus EXITING', 'Bus EXITED', - 'Waiting for child threads to terminate...']) - - def test_start_with_callback(self): - b = wspbus.Bus() - self.log(b) - try: - events = [] - def f(*args, **kwargs): - events.append(("f", args, kwargs)) - def g(): - events.append("g") - b.subscribe("start", g) - b.start_with_callback(f, (1, 3, 5), {"foo": "bar"}) - # Give wait() time to run f() - time.sleep(0.2) - - # The callback method MUST wait for the STARTED state. - self.assertEqual(b.state, b.states.STARTED) - # The callback method MUST run after all start methods. - self.assertEqual(events, ["g", ("f", (1, 3, 5), {"foo": "bar"})]) - finally: - b.exit() - - def test_log(self): - b = wspbus.Bus() - self.log(b) - self.assertLog([]) - - # Try a normal message. - expected = [] - for msg in ["O mah darlin'"] * 3 + ["Clementiiiiiiiine"]: - b.log(msg) - expected.append(msg) - self.assertLog(expected) - - # Try an error message - try: - foo - except NameError: - b.log("You are lost and gone forever", traceback=True) - lastmsg = self._log_entries[-1] - if "Traceback" not in lastmsg or "NameError" not in lastmsg: - self.fail("Last log message %r did not contain " - "the expected traceback." % lastmsg) - else: - self.fail("NameError was not raised as expected.") - - -if __name__ == "__main__": - unittest.main() diff --git a/libs/cherrypy/test/test_caching.py b/libs/cherrypy/test/test_caching.py deleted file mode 100644 index c210e6e..0000000 --- a/libs/cherrypy/test/test_caching.py +++ /dev/null @@ -1,328 +0,0 @@ -import datetime -import gzip -from itertools import count -import os -curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) -import sys -import threading -import time -import urllib - -import cherrypy -from cherrypy._cpcompat import next, ntob, quote, xrange -from cherrypy.lib import httputil - -gif_bytes = ntob('GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00' - '\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - '\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;') - - - -from cherrypy.test import helper - -class CacheTest(helper.CPWebCase): - - def setup_server(): - - class Root: - - _cp_config = {'tools.caching.on': True} - - def __init__(self): - self.counter = 0 - self.control_counter = 0 - self.longlock = threading.Lock() - - def index(self): - self.counter += 1 - msg = "visit #%s" % self.counter - return msg - index.exposed = True - - def control(self): - self.control_counter += 1 - return "visit #%s" % self.control_counter - control.exposed = True - - def a_gif(self): - cherrypy.response.headers['Last-Modified'] = httputil.HTTPDate() - return gif_bytes - a_gif.exposed = True - - def long_process(self, seconds='1'): - try: - self.longlock.acquire() - time.sleep(float(seconds)) - finally: - self.longlock.release() - return 'success!' - long_process.exposed = True - - def clear_cache(self, path): - cherrypy._cache.store[cherrypy.request.base + path].clear() - clear_cache.exposed = True - - class VaryHeaderCachingServer(object): - - _cp_config = {'tools.caching.on': True, - 'tools.response_headers.on': True, - 'tools.response_headers.headers': [('Vary', 'Our-Varying-Header')], - } - - def __init__(self): - self.counter = count(1) - - def index(self): - return "visit #%s" % next(self.counter) - index.exposed = True - - class UnCached(object): - _cp_config = {'tools.expires.on': True, - 'tools.expires.secs': 60, - 'tools.staticdir.on': True, - 'tools.staticdir.dir': 'static', - 'tools.staticdir.root': curdir, - } - - def force(self): - cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' - self._cp_config['tools.expires.force'] = True - self._cp_config['tools.expires.secs'] = 0 - return "being forceful" - force.exposed = True - force._cp_config = {'tools.expires.secs': 0} - - def dynamic(self): - cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' - cherrypy.response.headers['Cache-Control'] = 'private' - return "D-d-d-dynamic!" - dynamic.exposed = True - - def cacheable(self): - cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' - return "Hi, I'm cacheable." - cacheable.exposed = True - - def specific(self): - cherrypy.response.headers['Etag'] = 'need_this_to_make_me_cacheable' - return "I am being specific" - specific.exposed = True - specific._cp_config = {'tools.expires.secs': 86400} - - class Foo(object):pass - - def wrongtype(self): - cherrypy.response.headers['Etag'] = 'need_this_to_make_me_cacheable' - return "Woops" - wrongtype.exposed = True - wrongtype._cp_config = {'tools.expires.secs': Foo()} - - cherrypy.tree.mount(Root()) - cherrypy.tree.mount(UnCached(), "/expires") - cherrypy.tree.mount(VaryHeaderCachingServer(), "/varying_headers") - cherrypy.config.update({'tools.gzip.on': True}) - setup_server = staticmethod(setup_server) - - def testCaching(self): - elapsed = 0.0 - for trial in range(10): - self.getPage("/") - # The response should be the same every time, - # except for the Age response header. - self.assertBody('visit #1') - if trial != 0: - age = int(self.assertHeader("Age")) - self.assert_(age >= elapsed) - elapsed = age - - # POST, PUT, DELETE should not be cached. - self.getPage("/", method="POST") - self.assertBody('visit #2') - # Because gzip is turned on, the Vary header should always Vary for content-encoding - self.assertHeader('Vary', 'Accept-Encoding') - # The previous request should have invalidated the cache, - # so this request will recalc the response. - self.getPage("/", method="GET") - self.assertBody('visit #3') - # ...but this request should get the cached copy. - self.getPage("/", method="GET") - self.assertBody('visit #3') - self.getPage("/", method="DELETE") - self.assertBody('visit #4') - - # The previous request should have invalidated the cache, - # so this request will recalc the response. - self.getPage("/", method="GET", headers=[('Accept-Encoding', 'gzip')]) - self.assertHeader('Content-Encoding', 'gzip') - self.assertHeader('Vary') - self.assertEqual(cherrypy.lib.encoding.decompress(self.body), ntob("visit #5")) - - # Now check that a second request gets the gzip header and gzipped body - # This also tests a bug in 3.0 to 3.0.2 whereby the cached, gzipped - # response body was being gzipped a second time. - self.getPage("/", method="GET", headers=[('Accept-Encoding', 'gzip')]) - self.assertHeader('Content-Encoding', 'gzip') - self.assertEqual(cherrypy.lib.encoding.decompress(self.body), ntob("visit #5")) - - # Now check that a third request that doesn't accept gzip - # skips the cache (because the 'Vary' header denies it). - self.getPage("/", method="GET") - self.assertNoHeader('Content-Encoding') - self.assertBody('visit #6') - - def testVaryHeader(self): - self.getPage("/varying_headers/") - self.assertStatus("200 OK") - self.assertHeaderItemValue('Vary', 'Our-Varying-Header') - self.assertBody('visit #1') - - # Now check that different 'Vary'-fields don't evict each other. - # This test creates 2 requests with different 'Our-Varying-Header' - # and then tests if the first one still exists. - self.getPage("/varying_headers/", headers=[('Our-Varying-Header', 'request 2')]) - self.assertStatus("200 OK") - self.assertBody('visit #2') - - self.getPage("/varying_headers/", headers=[('Our-Varying-Header', 'request 2')]) - self.assertStatus("200 OK") - self.assertBody('visit #2') - - self.getPage("/varying_headers/") - self.assertStatus("200 OK") - self.assertBody('visit #1') - - def testExpiresTool(self): - # test setting an expires header - self.getPage("/expires/specific") - self.assertStatus("200 OK") - self.assertHeader("Expires") - - # test exceptions for bad time values - self.getPage("/expires/wrongtype") - self.assertStatus(500) - self.assertInBody("TypeError") - - # static content should not have "cache prevention" headers - self.getPage("/expires/index.html") - self.assertStatus("200 OK") - self.assertNoHeader("Pragma") - self.assertNoHeader("Cache-Control") - self.assertHeader("Expires") - - # dynamic content that sets indicators should not have - # "cache prevention" headers - self.getPage("/expires/cacheable") - self.assertStatus("200 OK") - self.assertNoHeader("Pragma") - self.assertNoHeader("Cache-Control") - self.assertHeader("Expires") - - self.getPage('/expires/dynamic') - self.assertBody("D-d-d-dynamic!") - # the Cache-Control header should be untouched - self.assertHeader("Cache-Control", "private") - self.assertHeader("Expires") - - # configure the tool to ignore indicators and replace existing headers - self.getPage("/expires/force") - self.assertStatus("200 OK") - # This also gives us a chance to test 0 expiry with no other headers - self.assertHeader("Pragma", "no-cache") - if cherrypy.server.protocol_version == "HTTP/1.1": - self.assertHeader("Cache-Control", "no-cache, must-revalidate") - self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT") - - # static content should now have "cache prevention" headers - self.getPage("/expires/index.html") - self.assertStatus("200 OK") - self.assertHeader("Pragma", "no-cache") - if cherrypy.server.protocol_version == "HTTP/1.1": - self.assertHeader("Cache-Control", "no-cache, must-revalidate") - self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT") - - # the cacheable handler should now have "cache prevention" headers - self.getPage("/expires/cacheable") - self.assertStatus("200 OK") - self.assertHeader("Pragma", "no-cache") - if cherrypy.server.protocol_version == "HTTP/1.1": - self.assertHeader("Cache-Control", "no-cache, must-revalidate") - self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT") - - self.getPage('/expires/dynamic') - self.assertBody("D-d-d-dynamic!") - # dynamic sets Cache-Control to private but it should be - # overwritten here ... - self.assertHeader("Pragma", "no-cache") - if cherrypy.server.protocol_version == "HTTP/1.1": - self.assertHeader("Cache-Control", "no-cache, must-revalidate") - self.assertHeader("Expires", "Sun, 28 Jan 2007 00:00:00 GMT") - - def testLastModified(self): - self.getPage("/a.gif") - self.assertStatus(200) - self.assertBody(gif_bytes) - lm1 = self.assertHeader("Last-Modified") - - # this request should get the cached copy. - self.getPage("/a.gif") - self.assertStatus(200) - self.assertBody(gif_bytes) - self.assertHeader("Age") - lm2 = self.assertHeader("Last-Modified") - self.assertEqual(lm1, lm2) - - # this request should match the cached copy, but raise 304. - self.getPage("/a.gif", [('If-Modified-Since', lm1)]) - self.assertStatus(304) - self.assertNoHeader("Last-Modified") - if not getattr(cherrypy.server, "using_apache", False): - self.assertHeader("Age") - - def test_antistampede(self): - SECONDS = 4 - # We MUST make an initial synchronous request in order to create the - # AntiStampedeCache object, and populate its selecting_headers, - # before the actual stampede. - self.getPage("/long_process?seconds=%d" % SECONDS) - self.assertBody('success!') - self.getPage("/clear_cache?path=" + - quote('/long_process?seconds=%d' % SECONDS, safe='')) - self.assertStatus(200) - - start = datetime.datetime.now() - def run(): - self.getPage("/long_process?seconds=%d" % SECONDS) - # The response should be the same every time - self.assertBody('success!') - ts = [threading.Thread(target=run) for i in xrange(100)] - for t in ts: - t.start() - for t in ts: - t.join() - self.assertEqualDates(start, datetime.datetime.now(), - # Allow a second (two, for slow hosts) - # for our thread/TCP overhead etc. - seconds=SECONDS + 2) - - def test_cache_control(self): - self.getPage("/control") - self.assertBody('visit #1') - self.getPage("/control") - self.assertBody('visit #1') - - self.getPage("/control", headers=[('Cache-Control', 'no-cache')]) - self.assertBody('visit #2') - self.getPage("/control") - self.assertBody('visit #2') - - self.getPage("/control", headers=[('Pragma', 'no-cache')]) - self.assertBody('visit #3') - self.getPage("/control") - self.assertBody('visit #3') - - time.sleep(1) - self.getPage("/control", headers=[('Cache-Control', 'max-age=0')]) - self.assertBody('visit #4') - self.getPage("/control") - self.assertBody('visit #4') - diff --git a/libs/cherrypy/test/test_config.py b/libs/cherrypy/test/test_config.py deleted file mode 100644 index b1ef6a3..0000000 --- a/libs/cherrypy/test/test_config.py +++ /dev/null @@ -1,256 +0,0 @@ -"""Tests for the CherryPy configuration system.""" - -import os, sys -localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - -from cherrypy._cpcompat import ntob, StringIO -import unittest - -import cherrypy - -def setup_server(): - - class Root: - - _cp_config = {'foo': 'this', - 'bar': 'that'} - - def __init__(self): - cherrypy.config.namespaces['db'] = self.db_namespace - - def db_namespace(self, k, v): - if k == "scheme": - self.db = v - - # @cherrypy.expose(alias=('global_', 'xyz')) - def index(self, key): - return cherrypy.request.config.get(key, "None") - index = cherrypy.expose(index, alias=('global_', 'xyz')) - - def repr(self, key): - return repr(cherrypy.request.config.get(key, None)) - repr.exposed = True - - def dbscheme(self): - return self.db - dbscheme.exposed = True - - def plain(self, x): - return x - plain.exposed = True - plain._cp_config = {'request.body.attempt_charsets': ['utf-16']} - - favicon_ico = cherrypy.tools.staticfile.handler( - filename=os.path.join(localDir, '../favicon.ico')) - - class Foo: - - _cp_config = {'foo': 'this2', - 'baz': 'that2'} - - def index(self, key): - return cherrypy.request.config.get(key, "None") - index.exposed = True - nex = index - - def silly(self): - return 'Hello world' - silly.exposed = True - silly._cp_config = {'response.headers.X-silly': 'sillyval'} - - # Test the expose and config decorators - #@cherrypy.expose - #@cherrypy.config(foo='this3', **{'bax': 'this4'}) - def bar(self, key): - return repr(cherrypy.request.config.get(key, None)) - bar.exposed = True - bar._cp_config = {'foo': 'this3', 'bax': 'this4'} - - class Another: - - def index(self, key): - return str(cherrypy.request.config.get(key, "None")) - index.exposed = True - - - def raw_namespace(key, value): - if key == 'input.map': - handler = cherrypy.request.handler - def wrapper(): - params = cherrypy.request.params - for name, coercer in list(value.items()): - try: - params[name] = coercer(params[name]) - except KeyError: - pass - return handler() - cherrypy.request.handler = wrapper - elif key == 'output': - handler = cherrypy.request.handler - def wrapper(): - # 'value' is a type (like int or str). - return value(handler()) - cherrypy.request.handler = wrapper - - class Raw: - - _cp_config = {'raw.output': repr} - - def incr(self, num): - return num + 1 - incr.exposed = True - incr._cp_config = {'raw.input.map': {'num': int}} - - ioconf = StringIO(""" -[/] -neg: -1234 -filename: os.path.join(sys.prefix, "hello.py") -thing1: cherrypy.lib.httputil.response_codes[404] -thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2 -complex: 3+2j -mul: 6*3 -ones: "11" -twos: "22" -stradd: %%(ones)s + %%(twos)s + "33" - -[/favicon.ico] -tools.staticfile.filename = %r -""" % os.path.join(localDir, 'static/dirback.jpg')) - - root = Root() - root.foo = Foo() - root.raw = Raw() - app = cherrypy.tree.mount(root, config=ioconf) - app.request_class.namespaces['raw'] = raw_namespace - - cherrypy.tree.mount(Another(), "/another") - cherrypy.config.update({'luxuryyacht': 'throatwobblermangrove', - 'db.scheme': r"sqlite///memory", - }) - - -# Client-side code # - -from cherrypy.test import helper - -class ConfigTests(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def testConfig(self): - tests = [ - ('/', 'nex', 'None'), - ('/', 'foo', 'this'), - ('/', 'bar', 'that'), - ('/xyz', 'foo', 'this'), - ('/foo/', 'foo', 'this2'), - ('/foo/', 'bar', 'that'), - ('/foo/', 'bax', 'None'), - ('/foo/bar', 'baz', "'that2'"), - ('/foo/nex', 'baz', 'that2'), - # If 'foo' == 'this', then the mount point '/another' leaks into '/'. - ('/another/','foo', 'None'), - ] - for path, key, expected in tests: - self.getPage(path + "?key=" + key) - self.assertBody(expected) - - expectedconf = { - # From CP defaults - 'tools.log_headers.on': False, - 'tools.log_tracebacks.on': True, - 'request.show_tracebacks': True, - 'log.screen': False, - 'environment': 'test_suite', - 'engine.autoreload_on': False, - # From global config - 'luxuryyacht': 'throatwobblermangrove', - # From Root._cp_config - 'bar': 'that', - # From Foo._cp_config - 'baz': 'that2', - # From Foo.bar._cp_config - 'foo': 'this3', - 'bax': 'this4', - } - for key, expected in expectedconf.items(): - self.getPage("/foo/bar?key=" + key) - self.assertBody(repr(expected)) - - def testUnrepr(self): - self.getPage("/repr?key=neg") - self.assertBody("-1234") - - self.getPage("/repr?key=filename") - self.assertBody(repr(os.path.join(sys.prefix, "hello.py"))) - - self.getPage("/repr?key=thing1") - self.assertBody(repr(cherrypy.lib.httputil.response_codes[404])) - - if not getattr(cherrypy.server, "using_apache", False): - # The object ID's won't match up when using Apache, since the - # server and client are running in different processes. - self.getPage("/repr?key=thing2") - from cherrypy.tutorial import thing2 - self.assertBody(repr(thing2)) - - self.getPage("/repr?key=complex") - self.assertBody("(3+2j)") - - self.getPage("/repr?key=mul") - self.assertBody("18") - - self.getPage("/repr?key=stradd") - self.assertBody(repr("112233")) - - def testRespNamespaces(self): - self.getPage("/foo/silly") - self.assertHeader('X-silly', 'sillyval') - self.assertBody('Hello world') - - def testCustomNamespaces(self): - self.getPage("/raw/incr?num=12") - self.assertBody("13") - - self.getPage("/dbscheme") - self.assertBody(r"sqlite///memory") - - def testHandlerToolConfigOverride(self): - # Assert that config overrides tool constructor args. Above, we set - # the favicon in the page handler to be '../favicon.ico', - # but then overrode it in config to be './static/dirback.jpg'. - self.getPage("/favicon.ico") - self.assertBody(open(os.path.join(localDir, "static/dirback.jpg"), - "rb").read()) - - def test_request_body_namespace(self): - self.getPage("/plain", method='POST', headers=[ - ('Content-Type', 'application/x-www-form-urlencoded'), - ('Content-Length', '13')], - body=ntob('\xff\xfex\x00=\xff\xfea\x00b\x00c\x00')) - self.assertBody("abc") - - -class VariableSubstitutionTests(unittest.TestCase): - setup_server = staticmethod(setup_server) - - def test_config(self): - from textwrap import dedent - - # variable substitution with [DEFAULT] - conf = dedent(""" - [DEFAULT] - dir = "/some/dir" - my.dir = %(dir)s + "/sub" - - [my] - my.dir = %(dir)s + "/my/dir" - my.dir2 = %(my.dir)s + '/dir2' - - """) - - fp = StringIO(conf) - - cherrypy.config.update(fp) - self.assertEqual(cherrypy.config["my"]["my.dir"], "/some/dir/my/dir") - self.assertEqual(cherrypy.config["my"]["my.dir2"], "/some/dir/my/dir/dir2") - diff --git a/libs/cherrypy/test/test_config_server.py b/libs/cherrypy/test/test_config_server.py deleted file mode 100644 index 0b9718d..0000000 --- a/libs/cherrypy/test/test_config_server.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Tests for the CherryPy configuration system.""" - -import os, sys -localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) -import socket -import time - -import cherrypy - - -# Client-side code # - -from cherrypy.test import helper - -class ServerConfigTests(helper.CPWebCase): - - def setup_server(): - - class Root: - def index(self): - return cherrypy.request.wsgi_environ['SERVER_PORT'] - index.exposed = True - - def upload(self, file): - return "Size: %s" % len(file.file.read()) - upload.exposed = True - - def tinyupload(self): - return cherrypy.request.body.read() - tinyupload.exposed = True - tinyupload._cp_config = {'request.body.maxbytes': 100} - - cherrypy.tree.mount(Root()) - - cherrypy.config.update({ - 'server.socket_host': '0.0.0.0', - 'server.socket_port': 9876, - 'server.max_request_body_size': 200, - 'server.max_request_header_size': 500, - 'server.socket_timeout': 0.5, - - # Test explicit server.instance - 'server.2.instance': 'cherrypy._cpwsgi_server.CPWSGIServer', - 'server.2.socket_port': 9877, - - # Test non-numeric - # Also test default server.instance = builtin server - 'server.yetanother.socket_port': 9878, - }) - setup_server = staticmethod(setup_server) - - PORT = 9876 - - def testBasicConfig(self): - self.getPage("/") - self.assertBody(str(self.PORT)) - - def testAdditionalServers(self): - if self.scheme == 'https': - return self.skip("not available under ssl") - self.PORT = 9877 - self.getPage("/") - self.assertBody(str(self.PORT)) - self.PORT = 9878 - self.getPage("/") - self.assertBody(str(self.PORT)) - - def testMaxRequestSizePerHandler(self): - if getattr(cherrypy.server, "using_apache", False): - return self.skip("skipped due to known Apache differences... ") - - self.getPage('/tinyupload', method="POST", - headers=[('Content-Type', 'text/plain'), - ('Content-Length', '100')], - body="x" * 100) - self.assertStatus(200) - self.assertBody("x" * 100) - - self.getPage('/tinyupload', method="POST", - headers=[('Content-Type', 'text/plain'), - ('Content-Length', '101')], - body="x" * 101) - self.assertStatus(413) - - def testMaxRequestSize(self): - if getattr(cherrypy.server, "using_apache", False): - return self.skip("skipped due to known Apache differences... ") - - for size in (500, 5000, 50000): - self.getPage("/", headers=[('From', "x" * 500)]) - self.assertStatus(413) - - # Test for http://www.cherrypy.org/ticket/421 - # (Incorrect border condition in readline of SizeCheckWrapper). - # This hangs in rev 891 and earlier. - lines256 = "x" * 248 - self.getPage("/", - headers=[('Host', '%s:%s' % (self.HOST, self.PORT)), - ('From', lines256)]) - - # Test upload - body = '\r\n'.join([ - '--x', - 'Content-Disposition: form-data; name="file"; filename="hello.txt"', - 'Content-Type: text/plain', - '', - '%s', - '--x--']) - partlen = 200 - len(body) - b = body % ("x" * partlen) - h = [("Content-type", "multipart/form-data; boundary=x"), - ("Content-Length", "%s" % len(b))] - self.getPage('/upload', h, "POST", b) - self.assertBody('Size: %d' % partlen) - - b = body % ("x" * 200) - h = [("Content-type", "multipart/form-data; boundary=x"), - ("Content-Length", "%s" % len(b))] - self.getPage('/upload', h, "POST", b) - self.assertStatus(413) - diff --git a/libs/cherrypy/test/test_conn.py b/libs/cherrypy/test/test_conn.py deleted file mode 100644 index 1346f59..0000000 --- a/libs/cherrypy/test/test_conn.py +++ /dev/null @@ -1,734 +0,0 @@ -"""Tests for TCP connection handling, including proper and timely close.""" - -import socket -import sys -import time -timeout = 1 - - -import cherrypy -from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, NotConnected, BadStatusLine -from cherrypy._cpcompat import ntob, urlopen, unicodestr -from cherrypy.test import webtest -from cherrypy import _cperror - - -pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN' - -def setup_server(): - - def raise500(): - raise cherrypy.HTTPError(500) - - class Root: - - def index(self): - return pov - index.exposed = True - page1 = index - page2 = index - page3 = index - - def hello(self): - return "Hello, world!" - hello.exposed = True - - def timeout(self, t): - return str(cherrypy.server.httpserver.timeout) - timeout.exposed = True - - def stream(self, set_cl=False): - if set_cl: - cherrypy.response.headers['Content-Length'] = 10 - - def content(): - for x in range(10): - yield str(x) - - return content() - stream.exposed = True - stream._cp_config = {'response.stream': True} - - def error(self, code=500): - raise cherrypy.HTTPError(code) - error.exposed = True - - def upload(self): - if not cherrypy.request.method == 'POST': - raise AssertionError("'POST' != request.method %r" % - cherrypy.request.method) - return "thanks for '%s'" % cherrypy.request.body.read() - upload.exposed = True - - def custom(self, response_code): - cherrypy.response.status = response_code - return "Code = %s" % response_code - custom.exposed = True - - def err_before_read(self): - return "ok" - err_before_read.exposed = True - err_before_read._cp_config = {'hooks.on_start_resource': raise500} - - def one_megabyte_of_a(self): - return ["a" * 1024] * 1024 - one_megabyte_of_a.exposed = True - - def custom_cl(self, body, cl): - cherrypy.response.headers['Content-Length'] = cl - if not isinstance(body, list): - body = [body] - newbody = [] - for chunk in body: - if isinstance(chunk, unicodestr): - chunk = chunk.encode('ISO-8859-1') - newbody.append(chunk) - return newbody - custom_cl.exposed = True - # Turn off the encoding tool so it doens't collapse - # our response body and reclaculate the Content-Length. - custom_cl._cp_config = {'tools.encode.on': False} - - cherrypy.tree.mount(Root()) - cherrypy.config.update({ - 'server.max_request_body_size': 1001, - 'server.socket_timeout': timeout, - }) - - -from cherrypy.test import helper - -class ConnectionCloseTests(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_HTTP11(self): - if cherrypy.server.protocol_version != "HTTP/1.1": - return self.skip() - - self.PROTOCOL = "HTTP/1.1" - - self.persistent = True - - # Make the first request and assert there's no "Connection: close". - self.getPage("/") - self.assertStatus('200 OK') - self.assertBody(pov) - self.assertNoHeader("Connection") - - # Make another request on the same connection. - self.getPage("/page1") - self.assertStatus('200 OK') - self.assertBody(pov) - self.assertNoHeader("Connection") - - # Test client-side close. - self.getPage("/page2", headers=[("Connection", "close")]) - self.assertStatus('200 OK') - self.assertBody(pov) - self.assertHeader("Connection", "close") - - # Make another request on the same connection, which should error. - self.assertRaises(NotConnected, self.getPage, "/") - - def test_Streaming_no_len(self): - self._streaming(set_cl=False) - - def test_Streaming_with_len(self): - self._streaming(set_cl=True) - - def _streaming(self, set_cl): - if cherrypy.server.protocol_version == "HTTP/1.1": - self.PROTOCOL = "HTTP/1.1" - - self.persistent = True - - # Make the first request and assert there's no "Connection: close". - self.getPage("/") - self.assertStatus('200 OK') - self.assertBody(pov) - self.assertNoHeader("Connection") - - # Make another, streamed request on the same connection. - if set_cl: - # When a Content-Length is provided, the content should stream - # without closing the connection. - self.getPage("/stream?set_cl=Yes") - self.assertHeader("Content-Length") - self.assertNoHeader("Connection", "close") - self.assertNoHeader("Transfer-Encoding") - - self.assertStatus('200 OK') - self.assertBody('0123456789') - else: - # When no Content-Length response header is provided, - # streamed output will either close the connection, or use - # chunked encoding, to determine transfer-length. - self.getPage("/stream") - self.assertNoHeader("Content-Length") - self.assertStatus('200 OK') - self.assertBody('0123456789') - - chunked_response = False - for k, v in self.headers: - if k.lower() == "transfer-encoding": - if str(v) == "chunked": - chunked_response = True - - if chunked_response: - self.assertNoHeader("Connection", "close") - else: - self.assertHeader("Connection", "close") - - # Make another request on the same connection, which should error. - self.assertRaises(NotConnected, self.getPage, "/") - - # Try HEAD. See http://www.cherrypy.org/ticket/864. - self.getPage("/stream", method='HEAD') - self.assertStatus('200 OK') - self.assertBody('') - self.assertNoHeader("Transfer-Encoding") - else: - self.PROTOCOL = "HTTP/1.0" - - self.persistent = True - - # Make the first request and assert Keep-Alive. - self.getPage("/", headers=[("Connection", "Keep-Alive")]) - self.assertStatus('200 OK') - self.assertBody(pov) - self.assertHeader("Connection", "Keep-Alive") - - # Make another, streamed request on the same connection. - if set_cl: - # When a Content-Length is provided, the content should - # stream without closing the connection. - self.getPage("/stream?set_cl=Yes", - headers=[("Connection", "Keep-Alive")]) - self.assertHeader("Content-Length") - self.assertHeader("Connection", "Keep-Alive") - self.assertNoHeader("Transfer-Encoding") - self.assertStatus('200 OK') - self.assertBody('0123456789') - else: - # When a Content-Length is not provided, - # the server should close the connection. - self.getPage("/stream", headers=[("Connection", "Keep-Alive")]) - self.assertStatus('200 OK') - self.assertBody('0123456789') - - self.assertNoHeader("Content-Length") - self.assertNoHeader("Connection", "Keep-Alive") - self.assertNoHeader("Transfer-Encoding") - - # Make another request on the same connection, which should error. - self.assertRaises(NotConnected, self.getPage, "/") - - def test_HTTP10_KeepAlive(self): - self.PROTOCOL = "HTTP/1.0" - if self.scheme == "https": - self.HTTP_CONN = HTTPSConnection - else: - self.HTTP_CONN = HTTPConnection - - # Test a normal HTTP/1.0 request. - self.getPage("/page2") - self.assertStatus('200 OK') - self.assertBody(pov) - # Apache, for example, may emit a Connection header even for HTTP/1.0 -## self.assertNoHeader("Connection") - - # Test a keep-alive HTTP/1.0 request. - self.persistent = True - - self.getPage("/page3", headers=[("Connection", "Keep-Alive")]) - self.assertStatus('200 OK') - self.assertBody(pov) - self.assertHeader("Connection", "Keep-Alive") - - # Remove the keep-alive header again. - self.getPage("/page3") - self.assertStatus('200 OK') - self.assertBody(pov) - # Apache, for example, may emit a Connection header even for HTTP/1.0 -## self.assertNoHeader("Connection") - - -class PipelineTests(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_HTTP11_Timeout(self): - # If we timeout without sending any data, - # the server will close the conn with a 408. - if cherrypy.server.protocol_version != "HTTP/1.1": - return self.skip() - - self.PROTOCOL = "HTTP/1.1" - - # Connect but send nothing. - self.persistent = True - conn = self.HTTP_CONN - conn.auto_open = False - conn.connect() - - # Wait for our socket timeout - time.sleep(timeout * 2) - - # The request should have returned 408 already. - response = conn.response_class(conn.sock, method="GET") - response.begin() - self.assertEqual(response.status, 408) - conn.close() - - # Connect but send half the headers only. - self.persistent = True - conn = self.HTTP_CONN - conn.auto_open = False - conn.connect() - conn.send(ntob('GET /hello HTTP/1.1')) - conn.send(("Host: %s" % self.HOST).encode('ascii')) - - # Wait for our socket timeout - time.sleep(timeout * 2) - - # The conn should have already sent 408. - response = conn.response_class(conn.sock, method="GET") - response.begin() - self.assertEqual(response.status, 408) - conn.close() - - def test_HTTP11_Timeout_after_request(self): - # If we timeout after at least one request has succeeded, - # the server will close the conn without 408. - if cherrypy.server.protocol_version != "HTTP/1.1": - return self.skip() - - self.PROTOCOL = "HTTP/1.1" - - # Make an initial request - self.persistent = True - conn = self.HTTP_CONN - conn.putrequest("GET", "/timeout?t=%s" % timeout, skip_host=True) - conn.putheader("Host", self.HOST) - conn.endheaders() - response = conn.response_class(conn.sock, method="GET") - response.begin() - self.assertEqual(response.status, 200) - self.body = response.read() - self.assertBody(str(timeout)) - - # Make a second request on the same socket - conn._output(ntob('GET /hello HTTP/1.1')) - conn._output(ntob("Host: %s" % self.HOST, 'ascii')) - conn._send_output() - response = conn.response_class(conn.sock, method="GET") - response.begin() - self.assertEqual(response.status, 200) - self.body = response.read() - self.assertBody("Hello, world!") - - # Wait for our socket timeout - time.sleep(timeout * 2) - - # Make another request on the same socket, which should error - conn._output(ntob('GET /hello HTTP/1.1')) - conn._output(ntob("Host: %s" % self.HOST, 'ascii')) - conn._send_output() - response = conn.response_class(conn.sock, method="GET") - try: - response.begin() - except: - if not isinstance(sys.exc_info()[1], - (socket.error, BadStatusLine)): - self.fail("Writing to timed out socket didn't fail" - " as it should have: %s" % sys.exc_info()[1]) - else: - if response.status != 408: - self.fail("Writing to timed out socket didn't fail" - " as it should have: %s" % - response.read()) - - conn.close() - - # Make another request on a new socket, which should work - self.persistent = True - conn = self.HTTP_CONN - conn.putrequest("GET", "/", skip_host=True) - conn.putheader("Host", self.HOST) - conn.endheaders() - response = conn.response_class(conn.sock, method="GET") - response.begin() - self.assertEqual(response.status, 200) - self.body = response.read() - self.assertBody(pov) - - - # Make another request on the same socket, - # but timeout on the headers - conn.send(ntob('GET /hello HTTP/1.1')) - # Wait for our socket timeout - time.sleep(timeout * 2) - response = conn.response_class(conn.sock, method="GET") - try: - response.begin() - except: - if not isinstance(sys.exc_info()[1], - (socket.error, BadStatusLine)): - self.fail("Writing to timed out socket didn't fail" - " as it should have: %s" % sys.exc_info()[1]) - else: - self.fail("Writing to timed out socket didn't fail" - " as it should have: %s" % - response.read()) - - conn.close() - - # Retry the request on a new connection, which should work - self.persistent = True - conn = self.HTTP_CONN - conn.putrequest("GET", "/", skip_host=True) - conn.putheader("Host", self.HOST) - conn.endheaders() - response = conn.response_class(conn.sock, method="GET") - response.begin() - self.assertEqual(response.status, 200) - self.body = response.read() - self.assertBody(pov) - conn.close() - - def test_HTTP11_pipelining(self): - if cherrypy.server.protocol_version != "HTTP/1.1": - return self.skip() - - self.PROTOCOL = "HTTP/1.1" - - # Test pipelining. httplib doesn't support this directly. - self.persistent = True - conn = self.HTTP_CONN - - # Put request 1 - conn.putrequest("GET", "/hello", skip_host=True) - conn.putheader("Host", self.HOST) - conn.endheaders() - - for trial in range(5): - # Put next request - conn._output(ntob('GET /hello HTTP/1.1')) - conn._output(ntob("Host: %s" % self.HOST, 'ascii')) - conn._send_output() - - # Retrieve previous response - response = conn.response_class(conn.sock, method="GET") - response.begin() - body = response.read(13) - self.assertEqual(response.status, 200) - self.assertEqual(body, ntob("Hello, world!")) - - # Retrieve final response - response = conn.response_class(conn.sock, method="GET") - response.begin() - body = response.read() - self.assertEqual(response.status, 200) - self.assertEqual(body, ntob("Hello, world!")) - - conn.close() - - def test_100_Continue(self): - if cherrypy.server.protocol_version != "HTTP/1.1": - return self.skip() - - self.PROTOCOL = "HTTP/1.1" - - self.persistent = True - conn = self.HTTP_CONN - - # Try a page without an Expect request header first. - # Note that httplib's response.begin automatically ignores - # 100 Continue responses, so we must manually check for it. - conn.putrequest("POST", "/upload", skip_host=True) - conn.putheader("Host", self.HOST) - conn.putheader("Content-Type", "text/plain") - conn.putheader("Content-Length", "4") - conn.endheaders() - conn.send(ntob("d'oh")) - response = conn.response_class(conn.sock, method="POST") - version, status, reason = response._read_status() - self.assertNotEqual(status, 100) - conn.close() - - # Now try a page with an Expect header... - conn.connect() - conn.putrequest("POST", "/upload", skip_host=True) - conn.putheader("Host", self.HOST) - conn.putheader("Content-Type", "text/plain") - conn.putheader("Content-Length", "17") - conn.putheader("Expect", "100-continue") - conn.endheaders() - response = conn.response_class(conn.sock, method="POST") - - # ...assert and then skip the 100 response - version, status, reason = response._read_status() - self.assertEqual(status, 100) - while True: - line = response.fp.readline().strip() - if line: - self.fail("100 Continue should not output any headers. Got %r" % line) - else: - break - - # ...send the body - body = ntob("I am a small file") - conn.send(body) - - # ...get the final response - response.begin() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus(200) - self.assertBody("thanks for '%s'" % body) - conn.close() - - -class ConnectionTests(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_readall_or_close(self): - if cherrypy.server.protocol_version != "HTTP/1.1": - return self.skip() - - self.PROTOCOL = "HTTP/1.1" - - if self.scheme == "https": - self.HTTP_CONN = HTTPSConnection - else: - self.HTTP_CONN = HTTPConnection - - # Test a max of 0 (the default) and then reset to what it was above. - old_max = cherrypy.server.max_request_body_size - for new_max in (0, old_max): - cherrypy.server.max_request_body_size = new_max - - self.persistent = True - conn = self.HTTP_CONN - - # Get a POST page with an error - conn.putrequest("POST", "/err_before_read", skip_host=True) - conn.putheader("Host", self.HOST) - conn.putheader("Content-Type", "text/plain") - conn.putheader("Content-Length", "1000") - conn.putheader("Expect", "100-continue") - conn.endheaders() - response = conn.response_class(conn.sock, method="POST") - - # ...assert and then skip the 100 response - version, status, reason = response._read_status() - self.assertEqual(status, 100) - while True: - skip = response.fp.readline().strip() - if not skip: - break - - # ...send the body - conn.send(ntob("x" * 1000)) - - # ...get the final response - response.begin() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus(500) - - # Now try a working page with an Expect header... - conn._output(ntob('POST /upload HTTP/1.1')) - conn._output(ntob("Host: %s" % self.HOST, 'ascii')) - conn._output(ntob("Content-Type: text/plain")) - conn._output(ntob("Content-Length: 17")) - conn._output(ntob("Expect: 100-continue")) - conn._send_output() - response = conn.response_class(conn.sock, method="POST") - - # ...assert and then skip the 100 response - version, status, reason = response._read_status() - self.assertEqual(status, 100) - while True: - skip = response.fp.readline().strip() - if not skip: - break - - # ...send the body - body = ntob("I am a small file") - conn.send(body) - - # ...get the final response - response.begin() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus(200) - self.assertBody("thanks for '%s'" % body) - conn.close() - - def test_No_Message_Body(self): - if cherrypy.server.protocol_version != "HTTP/1.1": - return self.skip() - - self.PROTOCOL = "HTTP/1.1" - - # Set our HTTP_CONN to an instance so it persists between requests. - self.persistent = True - - # Make the first request and assert there's no "Connection: close". - self.getPage("/") - self.assertStatus('200 OK') - self.assertBody(pov) - self.assertNoHeader("Connection") - - # Make a 204 request on the same connection. - self.getPage("/custom/204") - self.assertStatus(204) - self.assertNoHeader("Content-Length") - self.assertBody("") - self.assertNoHeader("Connection") - - # Make a 304 request on the same connection. - self.getPage("/custom/304") - self.assertStatus(304) - self.assertNoHeader("Content-Length") - self.assertBody("") - self.assertNoHeader("Connection") - - def test_Chunked_Encoding(self): - if cherrypy.server.protocol_version != "HTTP/1.1": - return self.skip() - - if (hasattr(self, 'harness') and - "modpython" in self.harness.__class__.__name__.lower()): - # mod_python forbids chunked encoding - return self.skip() - - self.PROTOCOL = "HTTP/1.1" - - # Set our HTTP_CONN to an instance so it persists between requests. - self.persistent = True - conn = self.HTTP_CONN - - # Try a normal chunked request (with extensions) - body = ntob("8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n" - "Content-Type: application/json\r\n" - "\r\n") - conn.putrequest("POST", "/upload", skip_host=True) - conn.putheader("Host", self.HOST) - conn.putheader("Transfer-Encoding", "chunked") - conn.putheader("Trailer", "Content-Type") - # Note that this is somewhat malformed: - # we shouldn't be sending Content-Length. - # RFC 2616 says the server should ignore it. - conn.putheader("Content-Length", "3") - conn.endheaders() - conn.send(body) - response = conn.getresponse() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus('200 OK') - self.assertBody("thanks for '%s'" % ntob('xx\r\nxxxxyyyyy')) - - # Try a chunked request that exceeds server.max_request_body_size. - # Note that the delimiters and trailer are included. - body = ntob("3e3\r\n" + ("x" * 995) + "\r\n0\r\n\r\n") - conn.putrequest("POST", "/upload", skip_host=True) - conn.putheader("Host", self.HOST) - conn.putheader("Transfer-Encoding", "chunked") - conn.putheader("Content-Type", "text/plain") - # Chunked requests don't need a content-length -## conn.putheader("Content-Length", len(body)) - conn.endheaders() - conn.send(body) - response = conn.getresponse() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus(413) - conn.close() - - def test_Content_Length_in(self): - # Try a non-chunked request where Content-Length exceeds - # server.max_request_body_size. Assert error before body send. - self.persistent = True - conn = self.HTTP_CONN - conn.putrequest("POST", "/upload", skip_host=True) - conn.putheader("Host", self.HOST) - conn.putheader("Content-Type", "text/plain") - conn.putheader("Content-Length", "9999") - conn.endheaders() - response = conn.getresponse() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus(413) - self.assertBody("The entity sent with the request exceeds " - "the maximum allowed bytes.") - conn.close() - - def test_Content_Length_out_preheaders(self): - # Try a non-chunked response where Content-Length is less than - # the actual bytes in the response body. - self.persistent = True - conn = self.HTTP_CONN - conn.putrequest("GET", "/custom_cl?body=I+have+too+many+bytes&cl=5", - skip_host=True) - conn.putheader("Host", self.HOST) - conn.endheaders() - response = conn.getresponse() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus(500) - self.assertBody( - "The requested resource returned more bytes than the " - "declared Content-Length.") - conn.close() - - def test_Content_Length_out_postheaders(self): - # Try a non-chunked response where Content-Length is less than - # the actual bytes in the response body. - self.persistent = True - conn = self.HTTP_CONN - conn.putrequest("GET", "/custom_cl?body=I+too&body=+have+too+many&cl=5", - skip_host=True) - conn.putheader("Host", self.HOST) - conn.endheaders() - response = conn.getresponse() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus(200) - self.assertBody("I too") - conn.close() - - def test_598(self): - remote_data_conn = urlopen('%s://%s:%s/one_megabyte_of_a/' % - (self.scheme, self.HOST, self.PORT,)) - buf = remote_data_conn.read(512) - time.sleep(timeout * 0.6) - remaining = (1024 * 1024) - 512 - while remaining: - data = remote_data_conn.read(remaining) - if not data: - break - else: - buf += data - remaining -= len(data) - - self.assertEqual(len(buf), 1024 * 1024) - self.assertEqual(buf, ntob("a" * 1024 * 1024)) - self.assertEqual(remaining, 0) - remote_data_conn.close() - - -class BadRequestTests(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_No_CRLF(self): - self.persistent = True - - conn = self.HTTP_CONN - conn.send(ntob('GET /hello HTTP/1.1\n\n')) - response = conn.response_class(conn.sock, method="GET") - response.begin() - self.body = response.read() - self.assertBody("HTTP requires CRLF terminators") - conn.close() - - conn.connect() - conn.send(ntob('GET /hello HTTP/1.1\r\n\n')) - response = conn.response_class(conn.sock, method="GET") - response.begin() - self.body = response.read() - self.assertBody("HTTP requires CRLF terminators") - conn.close() - diff --git a/libs/cherrypy/test/test_core.py b/libs/cherrypy/test/test_core.py deleted file mode 100644 index 0956a94..0000000 --- a/libs/cherrypy/test/test_core.py +++ /dev/null @@ -1,688 +0,0 @@ -"""Basic tests for the CherryPy core: request handling.""" - -import os -localDir = os.path.dirname(__file__) -import sys -import types - -import cherrypy -from cherrypy._cpcompat import IncompleteRead, itervalues, ntob -from cherrypy import _cptools, tools -from cherrypy.lib import httputil, static - - -favicon_path = os.path.join(os.getcwd(), localDir, "../favicon.ico") - -# Client-side code # - -from cherrypy.test import helper - -class CoreRequestHandlingTest(helper.CPWebCase): - - def setup_server(): - class Root: - - def index(self): - return "hello" - index.exposed = True - - favicon_ico = tools.staticfile.handler(filename=favicon_path) - - def defct(self, newct): - newct = "text/%s" % newct - cherrypy.config.update({'tools.response_headers.on': True, - 'tools.response_headers.headers': - [('Content-Type', newct)]}) - defct.exposed = True - - def baseurl(self, path_info, relative=None): - return cherrypy.url(path_info, relative=bool(relative)) - baseurl.exposed = True - - root = Root() - - if sys.version_info >= (2, 5): - from cherrypy.test._test_decorators import ExposeExamples - root.expose_dec = ExposeExamples() - - - class TestType(type): - """Metaclass which automatically exposes all functions in each subclass, - and adds an instance of the subclass as an attribute of root. - """ - def __init__(cls, name, bases, dct): - type.__init__(cls, name, bases, dct) - for value in itervalues(dct): - if isinstance(value, types.FunctionType): - value.exposed = True - setattr(root, name.lower(), cls()) - Test = TestType('Test', (object, ), {}) - - - class URL(Test): - - _cp_config = {'tools.trailing_slash.on': False} - - def index(self, path_info, relative=None): - if relative != 'server': - relative = bool(relative) - return cherrypy.url(path_info, relative=relative) - - def leaf(self, path_info, relative=None): - if relative != 'server': - relative = bool(relative) - return cherrypy.url(path_info, relative=relative) - - - def log_status(): - Status.statuses.append(cherrypy.response.status) - cherrypy.tools.log_status = cherrypy.Tool('on_end_resource', log_status) - - - class Status(Test): - - def index(self): - return "normal" - - def blank(self): - cherrypy.response.status = "" - - # According to RFC 2616, new status codes are OK as long as they - # are between 100 and 599. - - # Here is an illegal code... - def illegal(self): - cherrypy.response.status = 781 - return "oops" - - # ...and here is an unknown but legal code. - def unknown(self): - cherrypy.response.status = "431 My custom error" - return "funky" - - # Non-numeric code - def bad(self): - cherrypy.response.status = "error" - return "bad news" - - statuses = [] - def on_end_resource_stage(self): - return repr(self.statuses) - on_end_resource_stage._cp_config = {'tools.log_status.on': True} - - - class Redirect(Test): - - class Error: - _cp_config = {"tools.err_redirect.on": True, - "tools.err_redirect.url": "/errpage", - "tools.err_redirect.internal": False, - } - - def index(self): - raise NameError("redirect_test") - index.exposed = True - error = Error() - - def index(self): - return "child" - - def custom(self, url, code): - raise cherrypy.HTTPRedirect(url, code) - - def by_code(self, code): - raise cherrypy.HTTPRedirect("somewhere%20else", code) - by_code._cp_config = {'tools.trailing_slash.extra': True} - - def nomodify(self): - raise cherrypy.HTTPRedirect("", 304) - - def proxy(self): - raise cherrypy.HTTPRedirect("proxy", 305) - - def stringify(self): - return str(cherrypy.HTTPRedirect("/")) - - def fragment(self, frag): - raise cherrypy.HTTPRedirect("/some/url#%s" % frag) - - def login_redir(): - if not getattr(cherrypy.request, "login", None): - raise cherrypy.InternalRedirect("/internalredirect/login") - tools.login_redir = _cptools.Tool('before_handler', login_redir) - - def redir_custom(): - raise cherrypy.InternalRedirect("/internalredirect/custom_err") - - class InternalRedirect(Test): - - def index(self): - raise cherrypy.InternalRedirect("/") - - def choke(self): - return 3 / 0 - choke.exposed = True - choke._cp_config = {'hooks.before_error_response': redir_custom} - - def relative(self, a, b): - raise cherrypy.InternalRedirect("cousin?t=6") - - def cousin(self, t): - assert cherrypy.request.prev.closed - return cherrypy.request.prev.query_string - - def petshop(self, user_id): - if user_id == "parrot": - # Trade it for a slug when redirecting - raise cherrypy.InternalRedirect('/image/getImagesByUser?user_id=slug') - elif user_id == "terrier": - # Trade it for a fish when redirecting - raise cherrypy.InternalRedirect('/image/getImagesByUser?user_id=fish') - else: - # This should pass the user_id through to getImagesByUser - raise cherrypy.InternalRedirect( - '/image/getImagesByUser?user_id=%s' % str(user_id)) - - # We support Python 2.3, but the @-deco syntax would look like this: - # @tools.login_redir() - def secure(self): - return "Welcome!" - secure = tools.login_redir()(secure) - # Since calling the tool returns the same function you pass in, - # you could skip binding the return value, and just write: - # tools.login_redir()(secure) - - def login(self): - return "Please log in" - - def custom_err(self): - return "Something went horribly wrong." - - def early_ir(self, arg): - return "whatever" - early_ir._cp_config = {'hooks.before_request_body': redir_custom} - - - class Image(Test): - - def getImagesByUser(self, user_id): - return "0 images for %s" % user_id - - - class Flatten(Test): - - def as_string(self): - return "content" - - def as_list(self): - return ["con", "tent"] - - def as_yield(self): - yield ntob("content") - - def as_dblyield(self): - yield self.as_yield() - as_dblyield._cp_config = {'tools.flatten.on': True} - - def as_refyield(self): - for chunk in self.as_yield(): - yield chunk - - - class Ranges(Test): - - def get_ranges(self, bytes): - return repr(httputil.get_ranges('bytes=%s' % bytes, 8)) - - def slice_file(self): - path = os.path.join(os.getcwd(), os.path.dirname(__file__)) - return static.serve_file(os.path.join(path, "static/index.html")) - - - class Cookies(Test): - - def single(self, name): - cookie = cherrypy.request.cookie[name] - # Python2's SimpleCookie.__setitem__ won't take unicode keys. - cherrypy.response.cookie[str(name)] = cookie.value - - def multiple(self, names): - for name in names: - cookie = cherrypy.request.cookie[name] - # Python2's SimpleCookie.__setitem__ won't take unicode keys. - cherrypy.response.cookie[str(name)] = cookie.value - - def append_headers(header_list, debug=False): - if debug: - cherrypy.log( - "Extending response headers with %s" % repr(header_list), - "TOOLS.APPEND_HEADERS") - cherrypy.serving.response.header_list.extend(header_list) - cherrypy.tools.append_headers = cherrypy.Tool('on_end_resource', append_headers) - - class MultiHeader(Test): - - def header_list(self): - pass - header_list = cherrypy.tools.append_headers(header_list=[ - (ntob('WWW-Authenticate'), ntob('Negotiate')), - (ntob('WWW-Authenticate'), ntob('Basic realm="foo"')), - ])(header_list) - - def commas(self): - cherrypy.response.headers['WWW-Authenticate'] = 'Negotiate,Basic realm="foo"' - - - cherrypy.tree.mount(root) - setup_server = staticmethod(setup_server) - - - def testStatus(self): - self.getPage("/status/") - self.assertBody('normal') - self.assertStatus(200) - - self.getPage("/status/blank") - self.assertBody('') - self.assertStatus(200) - - self.getPage("/status/illegal") - self.assertStatus(500) - msg = "Illegal response status from server (781 is out of range)." - self.assertErrorPage(500, msg) - - if not getattr(cherrypy.server, 'using_apache', False): - self.getPage("/status/unknown") - self.assertBody('funky') - self.assertStatus(431) - - self.getPage("/status/bad") - self.assertStatus(500) - msg = "Illegal response status from server ('error' is non-numeric)." - self.assertErrorPage(500, msg) - - def test_on_end_resource_status(self): - self.getPage('/status/on_end_resource_stage') - self.assertBody('[]') - self.getPage('/status/on_end_resource_stage') - self.assertBody(repr(["200 OK"])) - - def testSlashes(self): - # Test that requests for index methods without a trailing slash - # get redirected to the same URI path with a trailing slash. - # Make sure GET params are preserved. - self.getPage("/redirect?id=3") - self.assertStatus(301) - self.assertInBody("" - "%s/redirect/?id=3" % (self.base(), self.base())) - - if self.prefix(): - # Corner case: the "trailing slash" redirect could be tricky if - # we're using a virtual root and the URI is "/vroot" (no slash). - self.getPage("") - self.assertStatus(301) - self.assertInBody("%s/" % - (self.base(), self.base())) - - # Test that requests for NON-index methods WITH a trailing slash - # get redirected to the same URI path WITHOUT a trailing slash. - # Make sure GET params are preserved. - self.getPage("/redirect/by_code/?code=307") - self.assertStatus(301) - self.assertInBody("" - "%s/redirect/by_code?code=307" - % (self.base(), self.base())) - - # If the trailing_slash tool is off, CP should just continue - # as if the slashes were correct. But it needs some help - # inside cherrypy.url to form correct output. - self.getPage('/url?path_info=page1') - self.assertBody('%s/url/page1' % self.base()) - self.getPage('/url/leaf/?path_info=page1') - self.assertBody('%s/url/page1' % self.base()) - - def testRedirect(self): - self.getPage("/redirect/") - self.assertBody('child') - self.assertStatus(200) - - self.getPage("/redirect/by_code?code=300") - self.assertMatchesBody(r"\1somewhere%20else") - self.assertStatus(300) - - self.getPage("/redirect/by_code?code=301") - self.assertMatchesBody(r"\1somewhere%20else") - self.assertStatus(301) - - self.getPage("/redirect/by_code?code=302") - self.assertMatchesBody(r"\1somewhere%20else") - self.assertStatus(302) - - self.getPage("/redirect/by_code?code=303") - self.assertMatchesBody(r"\1somewhere%20else") - self.assertStatus(303) - - self.getPage("/redirect/by_code?code=307") - self.assertMatchesBody(r"\1somewhere%20else") - self.assertStatus(307) - - self.getPage("/redirect/nomodify") - self.assertBody('') - self.assertStatus(304) - - self.getPage("/redirect/proxy") - self.assertBody('') - self.assertStatus(305) - - # HTTPRedirect on error - self.getPage("/redirect/error/") - self.assertStatus(('302 Found', '303 See Other')) - self.assertInBody('/errpage') - - # Make sure str(HTTPRedirect()) works. - self.getPage("/redirect/stringify", protocol="HTTP/1.0") - self.assertStatus(200) - self.assertBody("(['%s/'], 302)" % self.base()) - if cherrypy.server.protocol_version == "HTTP/1.1": - self.getPage("/redirect/stringify", protocol="HTTP/1.1") - self.assertStatus(200) - self.assertBody("(['%s/'], 303)" % self.base()) - - # check that #fragments are handled properly - # http://skrb.org/ietf/http_errata.html#location-fragments - frag = "foo" - self.getPage("/redirect/fragment/%s" % frag) - self.assertMatchesBody(r"\1\/some\/url\#%s" % (frag, frag)) - loc = self.assertHeader('Location') - assert loc.endswith("#%s" % frag) - self.assertStatus(('302 Found', '303 See Other')) - - # check injection protection - # See http://www.cherrypy.org/ticket/1003 - self.getPage("/redirect/custom?code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval") - self.assertStatus(303) - loc = self.assertHeader('Location') - assert 'Set-Cookie' in loc - self.assertNoHeader('Set-Cookie') - - def test_InternalRedirect(self): - # InternalRedirect - self.getPage("/internalredirect/") - self.assertBody('hello') - self.assertStatus(200) - - # Test passthrough - self.getPage("/internalredirect/petshop?user_id=Sir-not-appearing-in-this-film") - self.assertBody('0 images for Sir-not-appearing-in-this-film') - self.assertStatus(200) - - # Test args - self.getPage("/internalredirect/petshop?user_id=parrot") - self.assertBody('0 images for slug') - self.assertStatus(200) - - # Test POST - self.getPage("/internalredirect/petshop", method="POST", - body="user_id=terrier") - self.assertBody('0 images for fish') - self.assertStatus(200) - - # Test ir before body read - self.getPage("/internalredirect/early_ir", method="POST", - body="arg=aha!") - self.assertBody("Something went horribly wrong.") - self.assertStatus(200) - - self.getPage("/internalredirect/secure") - self.assertBody('Please log in') - self.assertStatus(200) - - # Relative path in InternalRedirect. - # Also tests request.prev. - self.getPage("/internalredirect/relative?a=3&b=5") - self.assertBody("a=3&b=5") - self.assertStatus(200) - - # InternalRedirect on error - self.getPage("/internalredirect/choke") - self.assertStatus(200) - self.assertBody("Something went horribly wrong.") - - def testFlatten(self): - for url in ["/flatten/as_string", "/flatten/as_list", - "/flatten/as_yield", "/flatten/as_dblyield", - "/flatten/as_refyield"]: - self.getPage(url) - self.assertBody('content') - - def testRanges(self): - self.getPage("/ranges/get_ranges?bytes=3-6") - self.assertBody("[(3, 7)]") - - # Test multiple ranges and a suffix-byte-range-spec, for good measure. - self.getPage("/ranges/get_ranges?bytes=2-4,-1") - self.assertBody("[(2, 5), (7, 8)]") - - # Get a partial file. - if cherrypy.server.protocol_version == "HTTP/1.1": - self.getPage("/ranges/slice_file", [('Range', 'bytes=2-5')]) - self.assertStatus(206) - self.assertHeader("Content-Type", "text/html;charset=utf-8") - self.assertHeader("Content-Range", "bytes 2-5/14") - self.assertBody("llo,") - - # What happens with overlapping ranges (and out of order, too)? - self.getPage("/ranges/slice_file", [('Range', 'bytes=4-6,2-5')]) - self.assertStatus(206) - ct = self.assertHeader("Content-Type") - expected_type = "multipart/byteranges; boundary=" - self.assert_(ct.startswith(expected_type)) - boundary = ct[len(expected_type):] - expected_body = ("\r\n--%s\r\n" - "Content-type: text/html\r\n" - "Content-range: bytes 4-6/14\r\n" - "\r\n" - "o, \r\n" - "--%s\r\n" - "Content-type: text/html\r\n" - "Content-range: bytes 2-5/14\r\n" - "\r\n" - "llo,\r\n" - "--%s--\r\n" % (boundary, boundary, boundary)) - self.assertBody(expected_body) - self.assertHeader("Content-Length") - - # Test "416 Requested Range Not Satisfiable" - self.getPage("/ranges/slice_file", [('Range', 'bytes=2300-2900')]) - self.assertStatus(416) - # "When this status code is returned for a byte-range request, - # the response SHOULD include a Content-Range entity-header - # field specifying the current length of the selected resource" - self.assertHeader("Content-Range", "bytes */14") - elif cherrypy.server.protocol_version == "HTTP/1.0": - # Test Range behavior with HTTP/1.0 request - self.getPage("/ranges/slice_file", [('Range', 'bytes=2-5')]) - self.assertStatus(200) - self.assertBody("Hello, world\r\n") - - def testFavicon(self): - # favicon.ico is served by staticfile. - icofilename = os.path.join(localDir, "../favicon.ico") - icofile = open(icofilename, "rb") - data = icofile.read() - icofile.close() - - self.getPage("/favicon.ico") - self.assertBody(data) - - def testCookies(self): - if sys.version_info >= (2, 5): - header_value = lambda x: x - else: - header_value = lambda x: x+';' - - self.getPage("/cookies/single?name=First", - [('Cookie', 'First=Dinsdale;')]) - self.assertHeader('Set-Cookie', header_value('First=Dinsdale')) - - self.getPage("/cookies/multiple?names=First&names=Last", - [('Cookie', 'First=Dinsdale; Last=Piranha;'), - ]) - self.assertHeader('Set-Cookie', header_value('First=Dinsdale')) - self.assertHeader('Set-Cookie', header_value('Last=Piranha')) - - self.getPage("/cookies/single?name=Something-With:Colon", - [('Cookie', 'Something-With:Colon=some-value')]) - self.assertStatus(400) - - def testDefaultContentType(self): - self.getPage('/') - self.assertHeader('Content-Type', 'text/html;charset=utf-8') - self.getPage('/defct/plain') - self.getPage('/') - self.assertHeader('Content-Type', 'text/plain;charset=utf-8') - self.getPage('/defct/html') - - def test_multiple_headers(self): - self.getPage('/multiheader/header_list') - self.assertEqual([(k, v) for k, v in self.headers if k == 'WWW-Authenticate'], - [('WWW-Authenticate', 'Negotiate'), - ('WWW-Authenticate', 'Basic realm="foo"'), - ]) - self.getPage('/multiheader/commas') - self.assertHeader('WWW-Authenticate', 'Negotiate,Basic realm="foo"') - - def test_cherrypy_url(self): - # Input relative to current - self.getPage('/url/leaf?path_info=page1') - self.assertBody('%s/url/page1' % self.base()) - self.getPage('/url/?path_info=page1') - self.assertBody('%s/url/page1' % self.base()) - # Other host header - host = 'www.mydomain.example' - self.getPage('/url/leaf?path_info=page1', - headers=[('Host', host)]) - self.assertBody('%s://%s/url/page1' % (self.scheme, host)) - - # Input is 'absolute'; that is, relative to script_name - self.getPage('/url/leaf?path_info=/page1') - self.assertBody('%s/page1' % self.base()) - self.getPage('/url/?path_info=/page1') - self.assertBody('%s/page1' % self.base()) - - # Single dots - self.getPage('/url/leaf?path_info=./page1') - self.assertBody('%s/url/page1' % self.base()) - self.getPage('/url/leaf?path_info=other/./page1') - self.assertBody('%s/url/other/page1' % self.base()) - self.getPage('/url/?path_info=/other/./page1') - self.assertBody('%s/other/page1' % self.base()) - - # Double dots - self.getPage('/url/leaf?path_info=../page1') - self.assertBody('%s/page1' % self.base()) - self.getPage('/url/leaf?path_info=other/../page1') - self.assertBody('%s/url/page1' % self.base()) - self.getPage('/url/leaf?path_info=/other/../page1') - self.assertBody('%s/page1' % self.base()) - - # Output relative to current path or script_name - self.getPage('/url/?path_info=page1&relative=True') - self.assertBody('page1') - self.getPage('/url/leaf?path_info=/page1&relative=True') - self.assertBody('../page1') - self.getPage('/url/leaf?path_info=page1&relative=True') - self.assertBody('page1') - self.getPage('/url/leaf?path_info=leaf/page1&relative=True') - self.assertBody('leaf/page1') - self.getPage('/url/leaf?path_info=../page1&relative=True') - self.assertBody('../page1') - self.getPage('/url/?path_info=other/../page1&relative=True') - self.assertBody('page1') - - # Output relative to / - self.getPage('/baseurl?path_info=ab&relative=True') - self.assertBody('ab') - # Output relative to / - self.getPage('/baseurl?path_info=/ab&relative=True') - self.assertBody('ab') - - # absolute-path references ("server-relative") - # Input relative to current - self.getPage('/url/leaf?path_info=page1&relative=server') - self.assertBody('/url/page1') - self.getPage('/url/?path_info=page1&relative=server') - self.assertBody('/url/page1') - # Input is 'absolute'; that is, relative to script_name - self.getPage('/url/leaf?path_info=/page1&relative=server') - self.assertBody('/page1') - self.getPage('/url/?path_info=/page1&relative=server') - self.assertBody('/page1') - - def test_expose_decorator(self): - if not sys.version_info >= (2, 5): - return self.skip("skipped (Python 2.5+ only) ") - - # Test @expose - self.getPage("/expose_dec/no_call") - self.assertStatus(200) - self.assertBody("Mr E. R. Bradshaw") - - # Test @expose() - self.getPage("/expose_dec/call_empty") - self.assertStatus(200) - self.assertBody("Mrs. B.J. Smegma") - - # Test @expose("alias") - self.getPage("/expose_dec/call_alias") - self.assertStatus(200) - self.assertBody("Mr Nesbitt") - # Does the original name work? - self.getPage("/expose_dec/nesbitt") - self.assertStatus(200) - self.assertBody("Mr Nesbitt") - - # Test @expose(["alias1", "alias2"]) - self.getPage("/expose_dec/alias1") - self.assertStatus(200) - self.assertBody("Mr Ken Andrews") - self.getPage("/expose_dec/alias2") - self.assertStatus(200) - self.assertBody("Mr Ken Andrews") - # Does the original name work? - self.getPage("/expose_dec/andrews") - self.assertStatus(200) - self.assertBody("Mr Ken Andrews") - - # Test @expose(alias="alias") - self.getPage("/expose_dec/alias3") - self.assertStatus(200) - self.assertBody("Mr. and Mrs. Watson") - - -class ErrorTests(helper.CPWebCase): - - def setup_server(): - def break_header(): - # Add a header after finalize that is invalid - cherrypy.serving.response.header_list.append((2, 3)) - cherrypy.tools.break_header = cherrypy.Tool('on_end_resource', break_header) - - class Root: - def index(self): - return "hello" - index.exposed = True - - def start_response_error(self): - return "salud!" - start_response_error._cp_config = {'tools.break_header.on': True} - root = Root() - - cherrypy.tree.mount(root) - setup_server = staticmethod(setup_server) - - def test_start_response_error(self): - self.getPage("/start_response_error") - self.assertStatus(500) - self.assertInBody("TypeError: response.header_list key 2 is not a byte string.") - diff --git a/libs/cherrypy/test/test_dynamicobjectmapping.py b/libs/cherrypy/test/test_dynamicobjectmapping.py deleted file mode 100644 index 0395b7b..0000000 --- a/libs/cherrypy/test/test_dynamicobjectmapping.py +++ /dev/null @@ -1,404 +0,0 @@ -import cherrypy -from cherrypy._cpcompat import sorted, unicodestr -from cherrypy._cptree import Application -from cherrypy.test import helper - -script_names = ["", "/foo", "/users/fred/blog", "/corp/blog"] - - - -def setup_server(): - class SubSubRoot: - def index(self): - return "SubSubRoot index" - index.exposed = True - - def default(self, *args): - return "SubSubRoot default" - default.exposed = True - - def handler(self): - return "SubSubRoot handler" - handler.exposed = True - - def dispatch(self): - return "SubSubRoot dispatch" - dispatch.exposed = True - - subsubnodes = { - '1': SubSubRoot(), - '2': SubSubRoot(), - } - - class SubRoot: - def index(self): - return "SubRoot index" - index.exposed = True - - def default(self, *args): - return "SubRoot %s" % (args,) - default.exposed = True - - def handler(self): - return "SubRoot handler" - handler.exposed = True - - def _cp_dispatch(self, vpath): - return subsubnodes.get(vpath[0], None) - - subnodes = { - '1': SubRoot(), - '2': SubRoot(), - } - class Root: - def index(self): - return "index" - index.exposed = True - - def default(self, *args): - return "default %s" % (args,) - default.exposed = True - - def handler(self): - return "handler" - handler.exposed = True - - def _cp_dispatch(self, vpath): - return subnodes.get(vpath[0]) - - #-------------------------------------------------------------------------- - # DynamicNodeAndMethodDispatcher example. - # This example exposes a fairly naive HTTP api - class User(object): - def __init__(self, id, name): - self.id = id - self.name = name - - def __unicode__(self): - return unicode(self.name) - def __str__(self): - return str(self.name) - - user_lookup = { - 1: User(1, 'foo'), - 2: User(2, 'bar'), - } - - def make_user(name, id=None): - if not id: - id = max(*list(user_lookup.keys())) + 1 - user_lookup[id] = User(id, name) - return id - - class UserContainerNode(object): - exposed = True - - def POST(self, name): - """ - Allow the creation of a new Object - """ - return "POST %d" % make_user(name) - - def GET(self): - return unicodestr(sorted(user_lookup.keys())) - - def dynamic_dispatch(self, vpath): - try: - id = int(vpath[0]) - except (ValueError, IndexError): - return None - return UserInstanceNode(id) - - class UserInstanceNode(object): - exposed = True - def __init__(self, id): - self.id = id - self.user = user_lookup.get(id, None) - - # For all but PUT methods there MUST be a valid user identified - # by self.id - if not self.user and cherrypy.request.method != 'PUT': - raise cherrypy.HTTPError(404) - - def GET(self, *args, **kwargs): - """ - Return the appropriate representation of the instance. - """ - return unicodestr(self.user) - - def POST(self, name): - """ - Update the fields of the user instance. - """ - self.user.name = name - return "POST %d" % self.user.id - - def PUT(self, name): - """ - Create a new user with the specified id, or edit it if it already exists - """ - if self.user: - # Edit the current user - self.user.name = name - return "PUT %d" % self.user.id - else: - # Make a new user with said attributes. - return "PUT %d" % make_user(name, self.id) - - def DELETE(self): - """ - Delete the user specified at the id. - """ - id = self.user.id - del user_lookup[self.user.id] - del self.user - return "DELETE %d" % id - - - class ABHandler: - class CustomDispatch: - def index(self, a, b): - return "custom" - index.exposed = True - - def _cp_dispatch(self, vpath): - """Make sure that if we don't pop anything from vpath, - processing still works. - """ - return self.CustomDispatch() - - def index(self, a, b=None): - body = [ 'a:' + str(a) ] - if b is not None: - body.append(',b:' + str(b)) - return ''.join(body) - index.exposed = True - - def delete(self, a, b): - return 'deleting ' + str(a) + ' and ' + str(b) - delete.exposed = True - - class IndexOnly: - def _cp_dispatch(self, vpath): - """Make sure that popping ALL of vpath still shows the index - handler. - """ - while vpath: - vpath.pop() - return self - - def index(self): - return "IndexOnly index" - index.exposed = True - - class DecoratedPopArgs: - """Test _cp_dispatch with @cherrypy.popargs.""" - def index(self): - return "no params" - index.exposed = True - - def hi(self): - return "hi was not interpreted as 'a' param" - hi.exposed = True - DecoratedPopArgs = cherrypy.popargs('a', 'b', handler=ABHandler())(DecoratedPopArgs) - - class NonDecoratedPopArgs: - """Test _cp_dispatch = cherrypy.popargs()""" - - _cp_dispatch = cherrypy.popargs('a') - - def index(self, a): - return "index: " + str(a) - index.exposed = True - - class ParameterizedHandler: - """Special handler created for each request""" - - def __init__(self, a): - self.a = a - - def index(self): - if 'a' in cherrypy.request.params: - raise Exception("Parameterized handler argument ended up in request.params") - return self.a - index.exposed = True - - class ParameterizedPopArgs: - """Test cherrypy.popargs() with a function call handler""" - ParameterizedPopArgs = cherrypy.popargs('a', handler=ParameterizedHandler)(ParameterizedPopArgs) - - Root.decorated = DecoratedPopArgs() - Root.undecorated = NonDecoratedPopArgs() - Root.index_only = IndexOnly() - Root.parameter_test = ParameterizedPopArgs() - - Root.users = UserContainerNode() - - md = cherrypy.dispatch.MethodDispatcher('dynamic_dispatch') - for url in script_names: - conf = {'/': { - 'user': (url or "/").split("/")[-2], - }, - '/users': { - 'request.dispatch': md - }, - } - cherrypy.tree.mount(Root(), url, conf) - -class DynamicObjectMappingTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def testObjectMapping(self): - for url in script_names: - prefix = self.script_name = url - - self.getPage('/') - self.assertBody('index') - - self.getPage('/handler') - self.assertBody('handler') - - # Dynamic dispatch will succeed here for the subnodes - # so the subroot gets called - self.getPage('/1/') - self.assertBody('SubRoot index') - - self.getPage('/2/') - self.assertBody('SubRoot index') - - self.getPage('/1/handler') - self.assertBody('SubRoot handler') - - self.getPage('/2/handler') - self.assertBody('SubRoot handler') - - # Dynamic dispatch will fail here for the subnodes - # so the default gets called - self.getPage('/asdf/') - self.assertBody("default ('asdf',)") - - self.getPage('/asdf/asdf') - self.assertBody("default ('asdf', 'asdf')") - - self.getPage('/asdf/handler') - self.assertBody("default ('asdf', 'handler')") - - # Dynamic dispatch will succeed here for the subsubnodes - # so the subsubroot gets called - self.getPage('/1/1/') - self.assertBody('SubSubRoot index') - - self.getPage('/2/2/') - self.assertBody('SubSubRoot index') - - self.getPage('/1/1/handler') - self.assertBody('SubSubRoot handler') - - self.getPage('/2/2/handler') - self.assertBody('SubSubRoot handler') - - self.getPage('/2/2/dispatch') - self.assertBody('SubSubRoot dispatch') - - # The exposed dispatch will not be called as a dispatch - # method. - self.getPage('/2/2/foo/foo') - self.assertBody("SubSubRoot default") - - # Dynamic dispatch will fail here for the subsubnodes - # so the SubRoot gets called - self.getPage('/1/asdf/') - self.assertBody("SubRoot ('asdf',)") - - self.getPage('/1/asdf/asdf') - self.assertBody("SubRoot ('asdf', 'asdf')") - - self.getPage('/1/asdf/handler') - self.assertBody("SubRoot ('asdf', 'handler')") - - def testMethodDispatch(self): - # GET acts like a container - self.getPage("/users") - self.assertBody("[1, 2]") - self.assertHeader('Allow', 'GET, HEAD, POST') - - # POST to the container URI allows creation - self.getPage("/users", method="POST", body="name=baz") - self.assertBody("POST 3") - self.assertHeader('Allow', 'GET, HEAD, POST') - - # POST to a specific instanct URI results in a 404 - # as the resource does not exit. - self.getPage("/users/5", method="POST", body="name=baz") - self.assertStatus(404) - - # PUT to a specific instanct URI results in creation - self.getPage("/users/5", method="PUT", body="name=boris") - self.assertBody("PUT 5") - self.assertHeader('Allow', 'DELETE, GET, HEAD, POST, PUT') - - # GET acts like a container - self.getPage("/users") - self.assertBody("[1, 2, 3, 5]") - self.assertHeader('Allow', 'GET, HEAD, POST') - - test_cases = ( - (1, 'foo', 'fooupdated', 'DELETE, GET, HEAD, POST, PUT'), - (2, 'bar', 'barupdated', 'DELETE, GET, HEAD, POST, PUT'), - (3, 'baz', 'bazupdated', 'DELETE, GET, HEAD, POST, PUT'), - (5, 'boris', 'borisupdated', 'DELETE, GET, HEAD, POST, PUT'), - ) - for id, name, updatedname, headers in test_cases: - self.getPage("/users/%d" % id) - self.assertBody(name) - self.assertHeader('Allow', headers) - - # Make sure POSTs update already existings resources - self.getPage("/users/%d" % id, method='POST', body="name=%s" % updatedname) - self.assertBody("POST %d" % id) - self.assertHeader('Allow', headers) - - # Make sure PUTs Update already existing resources. - self.getPage("/users/%d" % id, method='PUT', body="name=%s" % updatedname) - self.assertBody("PUT %d" % id) - self.assertHeader('Allow', headers) - - # Make sure DELETES Remove already existing resources. - self.getPage("/users/%d" % id, method='DELETE') - self.assertBody("DELETE %d" % id) - self.assertHeader('Allow', headers) - - - # GET acts like a container - self.getPage("/users") - self.assertBody("[]") - self.assertHeader('Allow', 'GET, HEAD, POST') - - def testVpathDispatch(self): - self.getPage("/decorated/") - self.assertBody("no params") - - self.getPage("/decorated/hi") - self.assertBody("hi was not interpreted as 'a' param") - - self.getPage("/decorated/yo/") - self.assertBody("a:yo") - - self.getPage("/decorated/yo/there/") - self.assertBody("a:yo,b:there") - - self.getPage("/decorated/yo/there/delete") - self.assertBody("deleting yo and there") - - self.getPage("/decorated/yo/there/handled_by_dispatch/") - self.assertBody("custom") - - self.getPage("/undecorated/blah/") - self.assertBody("index: blah") - - self.getPage("/index_only/a/b/c/d/e/f/g/") - self.assertBody("IndexOnly index") - - self.getPage("/parameter_test/argument2/") - self.assertBody("argument2") - diff --git a/libs/cherrypy/test/test_encoding.py b/libs/cherrypy/test/test_encoding.py deleted file mode 100644 index 2d0ce76..0000000 --- a/libs/cherrypy/test/test_encoding.py +++ /dev/null @@ -1,363 +0,0 @@ - -import gzip -import sys - -import cherrypy -from cherrypy._cpcompat import BytesIO, IncompleteRead, ntob, ntou - -europoundUnicode = ntou('\x80\xa3') -sing = ntou("\u6bdb\u6cfd\u4e1c: Sing, Little Birdie?", 'escape') -sing8 = sing.encode('utf-8') -sing16 = sing.encode('utf-16') - - -from cherrypy.test import helper - - -class EncodingTests(helper.CPWebCase): - - def setup_server(): - class Root: - def index(self, param): - assert param == europoundUnicode, "%r != %r" % (param, europoundUnicode) - yield europoundUnicode - index.exposed = True - - def mao_zedong(self): - return sing - mao_zedong.exposed = True - - def utf8(self): - return sing8 - utf8.exposed = True - utf8._cp_config = {'tools.encode.encoding': 'utf-8'} - - def cookies_and_headers(self): - # if the headers have non-ascii characters and a cookie has - # any part which is unicode (even ascii), the response - # should not fail. - cherrypy.response.cookie['candy'] = 'bar' - cherrypy.response.cookie['candy']['domain'] = 'cherrypy.org' - cherrypy.response.headers['Some-Header'] = 'My d\xc3\xb6g has fleas' - return 'Any content' - cookies_and_headers.exposed = True - - def reqparams(self, *args, **kwargs): - return ntob(', ').join([": ".join((k, v)).encode('utf8') - for k, v in cherrypy.request.params.items()]) - reqparams.exposed = True - - def nontext(self, *args, **kwargs): - cherrypy.response.headers['Content-Type'] = 'application/binary' - return '\x00\x01\x02\x03' - nontext.exposed = True - nontext._cp_config = {'tools.encode.text_only': False, - 'tools.encode.add_charset': True, - } - - class GZIP: - def index(self): - yield "Hello, world" - index.exposed = True - - def noshow(self): - # Test for ticket #147, where yield showed no exceptions (content- - # encoding was still gzip even though traceback wasn't zipped). - raise IndexError() - yield "Here be dragons" - noshow.exposed = True - # Turn encoding off so the gzip tool is the one doing the collapse. - noshow._cp_config = {'tools.encode.on': False} - - def noshow_stream(self): - # Test for ticket #147, where yield showed no exceptions (content- - # encoding was still gzip even though traceback wasn't zipped). - raise IndexError() - yield "Here be dragons" - noshow_stream.exposed = True - noshow_stream._cp_config = {'response.stream': True} - - class Decode: - def extra_charset(self, *args, **kwargs): - return ', '.join([": ".join((k, v)) - for k, v in cherrypy.request.params.items()]) - extra_charset.exposed = True - extra_charset._cp_config = { - 'tools.decode.on': True, - 'tools.decode.default_encoding': ['utf-16'], - } - - def force_charset(self, *args, **kwargs): - return ', '.join([": ".join((k, v)) - for k, v in cherrypy.request.params.items()]) - force_charset.exposed = True - force_charset._cp_config = { - 'tools.decode.on': True, - 'tools.decode.encoding': 'utf-16', - } - - root = Root() - root.gzip = GZIP() - root.decode = Decode() - cherrypy.tree.mount(root, config={'/gzip': {'tools.gzip.on': True}}) - setup_server = staticmethod(setup_server) - - def test_query_string_decoding(self): - europoundUtf8 = europoundUnicode.encode('utf-8') - self.getPage(ntob('/?param=') + europoundUtf8) - self.assertBody(europoundUtf8) - - # Encoded utf8 query strings MUST be parsed correctly. - # Here, q is the POUND SIGN U+00A3 encoded in utf8 and then %HEX - self.getPage("/reqparams?q=%C2%A3") - # The return value will be encoded as utf8. - self.assertBody(ntob("q: \xc2\xa3")) - - # Query strings that are incorrectly encoded MUST raise 404. - # Here, q is the POUND SIGN U+00A3 encoded in latin1 and then %HEX - self.getPage("/reqparams?q=%A3") - self.assertStatus(404) - self.assertErrorPage(404, - "The given query string could not be processed. Query " - "strings for this resource must be encoded with 'utf8'.") - - def test_urlencoded_decoding(self): - # Test the decoding of an application/x-www-form-urlencoded entity. - europoundUtf8 = europoundUnicode.encode('utf-8') - body=ntob("param=") + europoundUtf8 - self.getPage('/', method='POST', - headers=[("Content-Type", "application/x-www-form-urlencoded"), - ("Content-Length", str(len(body))), - ], - body=body), - self.assertBody(europoundUtf8) - - # Encoded utf8 entities MUST be parsed and decoded correctly. - # Here, q is the POUND SIGN U+00A3 encoded in utf8 - body = ntob("q=\xc2\xa3") - self.getPage('/reqparams', method='POST', - headers=[("Content-Type", "application/x-www-form-urlencoded"), - ("Content-Length", str(len(body))), - ], - body=body), - self.assertBody(ntob("q: \xc2\xa3")) - - # ...and in utf16, which is not in the default attempt_charsets list: - body = ntob("\xff\xfeq\x00=\xff\xfe\xa3\x00") - self.getPage('/reqparams', method='POST', - headers=[("Content-Type", "application/x-www-form-urlencoded;charset=utf-16"), - ("Content-Length", str(len(body))), - ], - body=body), - self.assertBody(ntob("q: \xc2\xa3")) - - # Entities that are incorrectly encoded MUST raise 400. - # Here, q is the POUND SIGN U+00A3 encoded in utf16, but - # the Content-Type incorrectly labels it utf-8. - body = ntob("\xff\xfeq\x00=\xff\xfe\xa3\x00") - self.getPage('/reqparams', method='POST', - headers=[("Content-Type", "application/x-www-form-urlencoded;charset=utf-8"), - ("Content-Length", str(len(body))), - ], - body=body), - self.assertStatus(400) - self.assertErrorPage(400, - "The request entity could not be decoded. The following charsets " - "were attempted: ['utf-8']") - - def test_decode_tool(self): - # An extra charset should be tried first, and succeed if it matches. - # Here, we add utf-16 as a charset and pass a utf-16 body. - body = ntob("\xff\xfeq\x00=\xff\xfe\xa3\x00") - self.getPage('/decode/extra_charset', method='POST', - headers=[("Content-Type", "application/x-www-form-urlencoded"), - ("Content-Length", str(len(body))), - ], - body=body), - self.assertBody(ntob("q: \xc2\xa3")) - - # An extra charset should be tried first, and continue to other default - # charsets if it doesn't match. - # Here, we add utf-16 as a charset but still pass a utf-8 body. - body = ntob("q=\xc2\xa3") - self.getPage('/decode/extra_charset', method='POST', - headers=[("Content-Type", "application/x-www-form-urlencoded"), - ("Content-Length", str(len(body))), - ], - body=body), - self.assertBody(ntob("q: \xc2\xa3")) - - # An extra charset should error if force is True and it doesn't match. - # Here, we force utf-16 as a charset but still pass a utf-8 body. - body = ntob("q=\xc2\xa3") - self.getPage('/decode/force_charset', method='POST', - headers=[("Content-Type", "application/x-www-form-urlencoded"), - ("Content-Length", str(len(body))), - ], - body=body), - self.assertErrorPage(400, - "The request entity could not be decoded. The following charsets " - "were attempted: ['utf-16']") - - def test_multipart_decoding(self): - # Test the decoding of a multipart entity when the charset (utf16) is - # explicitly given. - body=ntob('\r\n'.join(['--X', - 'Content-Type: text/plain;charset=utf-16', - 'Content-Disposition: form-data; name="text"', - '', - '\xff\xfea\x00b\x00\x1c c\x00', - '--X', - 'Content-Type: text/plain;charset=utf-16', - 'Content-Disposition: form-data; name="submit"', - '', - '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00', - '--X--'])) - self.getPage('/reqparams', method='POST', - headers=[("Content-Type", "multipart/form-data;boundary=X"), - ("Content-Length", str(len(body))), - ], - body=body), - self.assertBody(ntob("text: ab\xe2\x80\x9cc, submit: Create")) - - def test_multipart_decoding_no_charset(self): - # Test the decoding of a multipart entity when the charset (utf8) is - # NOT explicitly given, but is in the list of charsets to attempt. - body=ntob('\r\n'.join(['--X', - 'Content-Disposition: form-data; name="text"', - '', - '\xe2\x80\x9c', - '--X', - 'Content-Disposition: form-data; name="submit"', - '', - 'Create', - '--X--'])) - self.getPage('/reqparams', method='POST', - headers=[("Content-Type", "multipart/form-data;boundary=X"), - ("Content-Length", str(len(body))), - ], - body=body), - self.assertBody(ntob("text: \xe2\x80\x9c, submit: Create")) - - def test_multipart_decoding_no_successful_charset(self): - # Test the decoding of a multipart entity when the charset (utf16) is - # NOT explicitly given, and is NOT in the list of charsets to attempt. - body=ntob('\r\n'.join(['--X', - 'Content-Disposition: form-data; name="text"', - '', - '\xff\xfea\x00b\x00\x1c c\x00', - '--X', - 'Content-Disposition: form-data; name="submit"', - '', - '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00', - '--X--'])) - self.getPage('/reqparams', method='POST', - headers=[("Content-Type", "multipart/form-data;boundary=X"), - ("Content-Length", str(len(body))), - ], - body=body), - self.assertStatus(400) - self.assertErrorPage(400, - "The request entity could not be decoded. The following charsets " - "were attempted: ['us-ascii', 'utf-8']") - - def test_nontext(self): - self.getPage('/nontext') - self.assertHeader('Content-Type', 'application/binary;charset=utf-8') - self.assertBody('\x00\x01\x02\x03') - - def testEncoding(self): - # Default encoding should be utf-8 - self.getPage('/mao_zedong') - self.assertBody(sing8) - - # Ask for utf-16. - self.getPage('/mao_zedong', [('Accept-Charset', 'utf-16')]) - self.assertHeader('Content-Type', 'text/html;charset=utf-16') - self.assertBody(sing16) - - # Ask for multiple encodings. ISO-8859-1 should fail, and utf-16 - # should be produced. - self.getPage('/mao_zedong', [('Accept-Charset', - 'iso-8859-1;q=1, utf-16;q=0.5')]) - self.assertBody(sing16) - - # The "*" value should default to our default_encoding, utf-8 - self.getPage('/mao_zedong', [('Accept-Charset', '*;q=1, utf-7;q=.2')]) - self.assertBody(sing8) - - # Only allow iso-8859-1, which should fail and raise 406. - self.getPage('/mao_zedong', [('Accept-Charset', 'iso-8859-1, *;q=0')]) - self.assertStatus("406 Not Acceptable") - self.assertInBody("Your client sent this Accept-Charset header: " - "iso-8859-1, *;q=0. We tried these charsets: " - "iso-8859-1.") - - # Ask for x-mac-ce, which should be unknown. See ticket #569. - self.getPage('/mao_zedong', [('Accept-Charset', - 'us-ascii, ISO-8859-1, x-mac-ce')]) - self.assertStatus("406 Not Acceptable") - self.assertInBody("Your client sent this Accept-Charset header: " - "us-ascii, ISO-8859-1, x-mac-ce. We tried these " - "charsets: ISO-8859-1, us-ascii, x-mac-ce.") - - # Test the 'encoding' arg to encode. - self.getPage('/utf8') - self.assertBody(sing8) - self.getPage('/utf8', [('Accept-Charset', 'us-ascii, ISO-8859-1')]) - self.assertStatus("406 Not Acceptable") - - def testGzip(self): - zbuf = BytesIO() - zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9) - zfile.write(ntob("Hello, world")) - zfile.close() - - self.getPage('/gzip/', headers=[("Accept-Encoding", "gzip")]) - self.assertInBody(zbuf.getvalue()[:3]) - self.assertHeader("Vary", "Accept-Encoding") - self.assertHeader("Content-Encoding", "gzip") - - # Test when gzip is denied. - self.getPage('/gzip/', headers=[("Accept-Encoding", "identity")]) - self.assertHeader("Vary", "Accept-Encoding") - self.assertNoHeader("Content-Encoding") - self.assertBody("Hello, world") - - self.getPage('/gzip/', headers=[("Accept-Encoding", "gzip;q=0")]) - self.assertHeader("Vary", "Accept-Encoding") - self.assertNoHeader("Content-Encoding") - self.assertBody("Hello, world") - - self.getPage('/gzip/', headers=[("Accept-Encoding", "*;q=0")]) - self.assertStatus(406) - self.assertNoHeader("Content-Encoding") - self.assertErrorPage(406, "identity, gzip") - - # Test for ticket #147 - self.getPage('/gzip/noshow', headers=[("Accept-Encoding", "gzip")]) - self.assertNoHeader('Content-Encoding') - self.assertStatus(500) - self.assertErrorPage(500, pattern="IndexError\n") - - # In this case, there's nothing we can do to deliver a - # readable page, since 1) the gzip header is already set, - # and 2) we may have already written some of the body. - # The fix is to never stream yields when using gzip. - if (cherrypy.server.protocol_version == "HTTP/1.0" or - getattr(cherrypy.server, "using_apache", False)): - self.getPage('/gzip/noshow_stream', - headers=[("Accept-Encoding", "gzip")]) - self.assertHeader('Content-Encoding', 'gzip') - self.assertInBody('\x1f\x8b\x08\x00') - else: - # The wsgiserver will simply stop sending data, and the HTTP client - # will error due to an incomplete chunk-encoded stream. - self.assertRaises((ValueError, IncompleteRead), self.getPage, - '/gzip/noshow_stream', - headers=[("Accept-Encoding", "gzip")]) - - def test_UnicodeHeaders(self): - self.getPage('/cookies_and_headers') - self.assertBody('Any content') - diff --git a/libs/cherrypy/test/test_etags.py b/libs/cherrypy/test/test_etags.py deleted file mode 100644 index aec1693..0000000 --- a/libs/cherrypy/test/test_etags.py +++ /dev/null @@ -1,83 +0,0 @@ -import cherrypy -from cherrypy._cpcompat import ntou -from cherrypy.test import helper - - -class ETagTest(helper.CPWebCase): - - def setup_server(): - class Root: - def resource(self): - return "Oh wah ta goo Siam." - resource.exposed = True - - def fail(self, code): - code = int(code) - if 300 <= code <= 399: - raise cherrypy.HTTPRedirect([], code) - else: - raise cherrypy.HTTPError(code) - fail.exposed = True - - def unicoded(self): - return ntou('I am a \u1ee4nicode string.', 'escape') - unicoded.exposed = True - # In Python 3, tools.encode is on by default - unicoded._cp_config = {'tools.encode.on': True} - - conf = {'/': {'tools.etags.on': True, - 'tools.etags.autotags': True, - }} - cherrypy.tree.mount(Root(), config=conf) - setup_server = staticmethod(setup_server) - - def test_etags(self): - self.getPage("/resource") - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/html;charset=utf-8') - self.assertBody('Oh wah ta goo Siam.') - etag = self.assertHeader('ETag') - - # Test If-Match (both valid and invalid) - self.getPage("/resource", headers=[('If-Match', etag)]) - self.assertStatus("200 OK") - self.getPage("/resource", headers=[('If-Match', "*")]) - self.assertStatus("200 OK") - self.getPage("/resource", headers=[('If-Match', "*")], method="POST") - self.assertStatus("200 OK") - self.getPage("/resource", headers=[('If-Match', "a bogus tag")]) - self.assertStatus("412 Precondition Failed") - - # Test If-None-Match (both valid and invalid) - self.getPage("/resource", headers=[('If-None-Match', etag)]) - self.assertStatus(304) - self.getPage("/resource", method='POST', headers=[('If-None-Match', etag)]) - self.assertStatus("412 Precondition Failed") - self.getPage("/resource", headers=[('If-None-Match', "*")]) - self.assertStatus(304) - self.getPage("/resource", headers=[('If-None-Match', "a bogus tag")]) - self.assertStatus("200 OK") - - def test_errors(self): - self.getPage("/resource") - self.assertStatus(200) - etag = self.assertHeader('ETag') - - # Test raising errors in page handler - self.getPage("/fail/412", headers=[('If-Match', etag)]) - self.assertStatus(412) - self.getPage("/fail/304", headers=[('If-Match', etag)]) - self.assertStatus(304) - self.getPage("/fail/412", headers=[('If-None-Match', "*")]) - self.assertStatus(412) - self.getPage("/fail/304", headers=[('If-None-Match', "*")]) - self.assertStatus(304) - - def test_unicode_body(self): - self.getPage("/unicoded") - self.assertStatus(200) - etag1 = self.assertHeader('ETag') - self.getPage("/unicoded", headers=[('If-Match', etag1)]) - self.assertStatus(200) - self.assertHeader('ETag', etag1) - diff --git a/libs/cherrypy/test/test_http.py b/libs/cherrypy/test/test_http.py deleted file mode 100644 index 639c6c4..0000000 --- a/libs/cherrypy/test/test_http.py +++ /dev/null @@ -1,212 +0,0 @@ -"""Tests for managing HTTP issues (malformed requests, etc).""" - -import errno -import mimetypes -import socket -import sys - -import cherrypy -from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob, py3k - - -def encode_multipart_formdata(files): - """Return (content_type, body) ready for httplib.HTTP instance. - - files: a sequence of (name, filename, value) tuples for multipart uploads. - """ - BOUNDARY = '________ThIs_Is_tHe_bouNdaRY_$' - L = [] - for key, filename, value in files: - L.append('--' + BOUNDARY) - L.append('Content-Disposition: form-data; name="%s"; filename="%s"' % - (key, filename)) - ct = mimetypes.guess_type(filename)[0] or 'application/octet-stream' - L.append('Content-Type: %s' % ct) - L.append('') - L.append(value) - L.append('--' + BOUNDARY + '--') - L.append('') - body = '\r\n'.join(L) - content_type = 'multipart/form-data; boundary=%s' % BOUNDARY - return content_type, body - - - - -from cherrypy.test import helper - -class HTTPTests(helper.CPWebCase): - - def setup_server(): - class Root: - def index(self, *args, **kwargs): - return "Hello world!" - index.exposed = True - - def no_body(self, *args, **kwargs): - return "Hello world!" - no_body.exposed = True - no_body._cp_config = {'request.process_request_body': False} - - def post_multipart(self, file): - """Return a summary ("a * 65536\nb * 65536") of the uploaded file.""" - contents = file.file.read() - summary = [] - curchar = None - count = 0 - for c in contents: - if c == curchar: - count += 1 - else: - if count: - if py3k: curchar = chr(curchar) - summary.append("%s * %d" % (curchar, count)) - count = 1 - curchar = c - if count: - if py3k: curchar = chr(curchar) - summary.append("%s * %d" % (curchar, count)) - return ", ".join(summary) - post_multipart.exposed = True - - cherrypy.tree.mount(Root()) - cherrypy.config.update({'server.max_request_body_size': 30000000}) - setup_server = staticmethod(setup_server) - - def test_no_content_length(self): - # "The presence of a message-body in a request is signaled by the - # inclusion of a Content-Length or Transfer-Encoding header field in - # the request's message-headers." - # - # Send a message with neither header and no body. Even though - # the request is of method POST, this should be OK because we set - # request.process_request_body to False for our handler. - if self.scheme == "https": - c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) - else: - c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) - c.request("POST", "/no_body") - response = c.getresponse() - self.body = response.fp.read() - self.status = str(response.status) - self.assertStatus(200) - self.assertBody(ntob('Hello world!')) - - # Now send a message that has no Content-Length, but does send a body. - # Verify that CP times out the socket and responds - # with 411 Length Required. - if self.scheme == "https": - c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) - else: - c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) - c.request("POST", "/") - response = c.getresponse() - self.body = response.fp.read() - self.status = str(response.status) - self.assertStatus(411) - - def test_post_multipart(self): - alphabet = "abcdefghijklmnopqrstuvwxyz" - # generate file contents for a large post - contents = "".join([c * 65536 for c in alphabet]) - - # encode as multipart form data - files=[('file', 'file.txt', contents)] - content_type, body = encode_multipart_formdata(files) - body = body.encode('Latin-1') - - # post file - if self.scheme == 'https': - c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) - else: - c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) - c.putrequest('POST', '/post_multipart') - c.putheader('Content-Type', content_type) - c.putheader('Content-Length', str(len(body))) - c.endheaders() - c.send(body) - - response = c.getresponse() - self.body = response.fp.read() - self.status = str(response.status) - self.assertStatus(200) - self.assertBody(", ".join(["%s * 65536" % c for c in alphabet])) - - def test_malformed_request_line(self): - if getattr(cherrypy.server, "using_apache", False): - return self.skip("skipped due to known Apache differences...") - - # Test missing version in Request-Line - if self.scheme == 'https': - c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) - else: - c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) - c._output(ntob('GET /')) - c._send_output() - if hasattr(c, 'strict'): - response = c.response_class(c.sock, strict=c.strict, method='GET') - else: - # Python 3.2 removed the 'strict' feature, saying: - # "http.client now always assumes HTTP/1.x compliant servers." - response = c.response_class(c.sock, method='GET') - response.begin() - self.assertEqual(response.status, 400) - self.assertEqual(response.fp.read(22), ntob("Malformed Request-Line")) - c.close() - - def test_malformed_header(self): - if self.scheme == 'https': - c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) - else: - c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) - c.putrequest('GET', '/') - c.putheader('Content-Type', 'text/plain') - # See http://www.cherrypy.org/ticket/941 - c._output(ntob('Re, 1.2.3.4#015#012')) - c.endheaders() - - response = c.getresponse() - self.status = str(response.status) - self.assertStatus(400) - self.body = response.fp.read(20) - self.assertBody("Illegal header line.") - - def test_http_over_https(self): - if self.scheme != 'https': - return self.skip("skipped (not running HTTPS)... ") - - # Try connecting without SSL. - conn = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) - conn.putrequest("GET", "/", skip_host=True) - conn.putheader("Host", self.HOST) - conn.endheaders() - response = conn.response_class(conn.sock, method="GET") - try: - response.begin() - self.assertEqual(response.status, 400) - self.body = response.read() - self.assertBody("The client sent a plain HTTP request, but this " - "server only speaks HTTPS on this port.") - except socket.error: - e = sys.exc_info()[1] - # "Connection reset by peer" is also acceptable. - if e.errno != errno.ECONNRESET: - raise - - def test_garbage_in(self): - # Connect without SSL regardless of server.scheme - c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) - c._output(ntob('gjkgjklsgjklsgjkljklsg')) - c._send_output() - response = c.response_class(c.sock, method="GET") - try: - response.begin() - self.assertEqual(response.status, 400) - self.assertEqual(response.fp.read(22), ntob("Malformed Request-Line")) - c.close() - except socket.error: - e = sys.exc_info()[1] - # "Connection reset by peer" is also acceptable. - if e.errno != errno.ECONNRESET: - raise - diff --git a/libs/cherrypy/test/test_httpauth.py b/libs/cherrypy/test/test_httpauth.py deleted file mode 100644 index 9d0eecb..0000000 --- a/libs/cherrypy/test/test_httpauth.py +++ /dev/null @@ -1,151 +0,0 @@ -import cherrypy -from cherrypy._cpcompat import md5, sha, ntob -from cherrypy.lib import httpauth - -from cherrypy.test import helper - -class HTTPAuthTest(helper.CPWebCase): - - def setup_server(): - class Root: - def index(self): - return "This is public." - index.exposed = True - - class DigestProtected: - def index(self): - return "Hello %s, you've been authorized." % cherrypy.request.login - index.exposed = True - - class BasicProtected: - def index(self): - return "Hello %s, you've been authorized." % cherrypy.request.login - index.exposed = True - - class BasicProtected2: - def index(self): - return "Hello %s, you've been authorized." % cherrypy.request.login - index.exposed = True - - def fetch_users(): - return {'test': 'test'} - - def sha_password_encrypter(password): - return sha(ntob(password)).hexdigest() - - def fetch_password(username): - return sha(ntob('test')).hexdigest() - - conf = {'/digest': {'tools.digest_auth.on': True, - 'tools.digest_auth.realm': 'localhost', - 'tools.digest_auth.users': fetch_users}, - '/basic': {'tools.basic_auth.on': True, - 'tools.basic_auth.realm': 'localhost', - 'tools.basic_auth.users': {'test': md5(ntob('test')).hexdigest()}}, - '/basic2': {'tools.basic_auth.on': True, - 'tools.basic_auth.realm': 'localhost', - 'tools.basic_auth.users': fetch_password, - 'tools.basic_auth.encrypt': sha_password_encrypter}} - - root = Root() - root.digest = DigestProtected() - root.basic = BasicProtected() - root.basic2 = BasicProtected2() - cherrypy.tree.mount(root, config=conf) - setup_server = staticmethod(setup_server) - - - def testPublic(self): - self.getPage("/") - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/html;charset=utf-8') - self.assertBody('This is public.') - - def testBasic(self): - self.getPage("/basic/") - self.assertStatus(401) - self.assertHeader('WWW-Authenticate', 'Basic realm="localhost"') - - self.getPage('/basic/', [('Authorization', 'Basic dGVzdDp0ZX60')]) - self.assertStatus(401) - - self.getPage('/basic/', [('Authorization', 'Basic dGVzdDp0ZXN0')]) - self.assertStatus('200 OK') - self.assertBody("Hello test, you've been authorized.") - - def testBasic2(self): - self.getPage("/basic2/") - self.assertStatus(401) - self.assertHeader('WWW-Authenticate', 'Basic realm="localhost"') - - self.getPage('/basic2/', [('Authorization', 'Basic dGVzdDp0ZX60')]) - self.assertStatus(401) - - self.getPage('/basic2/', [('Authorization', 'Basic dGVzdDp0ZXN0')]) - self.assertStatus('200 OK') - self.assertBody("Hello test, you've been authorized.") - - def testDigest(self): - self.getPage("/digest/") - self.assertStatus(401) - - value = None - for k, v in self.headers: - if k.lower() == "www-authenticate": - if v.startswith("Digest"): - value = v - break - - if value is None: - self._handlewebError("Digest authentification scheme was not found") - - value = value[7:] - items = value.split(', ') - tokens = {} - for item in items: - key, value = item.split('=') - tokens[key.lower()] = value - - missing_msg = "%s is missing" - bad_value_msg = "'%s' was expecting '%s' but found '%s'" - nonce = None - if 'realm' not in tokens: - self._handlewebError(missing_msg % 'realm') - elif tokens['realm'] != '"localhost"': - self._handlewebError(bad_value_msg % ('realm', '"localhost"', tokens['realm'])) - if 'nonce' not in tokens: - self._handlewebError(missing_msg % 'nonce') - else: - nonce = tokens['nonce'].strip('"') - if 'algorithm' not in tokens: - self._handlewebError(missing_msg % 'algorithm') - elif tokens['algorithm'] != '"MD5"': - self._handlewebError(bad_value_msg % ('algorithm', '"MD5"', tokens['algorithm'])) - if 'qop' not in tokens: - self._handlewebError(missing_msg % 'qop') - elif tokens['qop'] != '"auth"': - self._handlewebError(bad_value_msg % ('qop', '"auth"', tokens['qop'])) - - # Test a wrong 'realm' value - base_auth = 'Digest username="test", realm="wrong realm", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"' - - auth = base_auth % (nonce, '', '00000001') - params = httpauth.parseAuthorization(auth) - response = httpauth._computeDigestResponse(params, 'test') - - auth = base_auth % (nonce, response, '00000001') - self.getPage('/digest/', [('Authorization', auth)]) - self.assertStatus(401) - - # Test that must pass - base_auth = 'Digest username="test", realm="localhost", nonce="%s", uri="/digest/", algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"' - - auth = base_auth % (nonce, '', '00000001') - params = httpauth.parseAuthorization(auth) - response = httpauth._computeDigestResponse(params, 'test') - - auth = base_auth % (nonce, response, '00000001') - self.getPage('/digest/', [('Authorization', auth)]) - self.assertStatus('200 OK') - self.assertBody("Hello test, you've been authorized.") - diff --git a/libs/cherrypy/test/test_httplib.py b/libs/cherrypy/test/test_httplib.py deleted file mode 100644 index 5dc40fd..0000000 --- a/libs/cherrypy/test/test_httplib.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Tests for cherrypy/lib/httputil.py.""" - -import unittest -from cherrypy.lib import httputil - - -class UtilityTests(unittest.TestCase): - - def test_urljoin(self): - # Test all slash+atom combinations for SCRIPT_NAME and PATH_INFO - self.assertEqual(httputil.urljoin("/sn/", "/pi/"), "/sn/pi/") - self.assertEqual(httputil.urljoin("/sn/", "/pi"), "/sn/pi") - self.assertEqual(httputil.urljoin("/sn/", "/"), "/sn/") - self.assertEqual(httputil.urljoin("/sn/", ""), "/sn/") - self.assertEqual(httputil.urljoin("/sn", "/pi/"), "/sn/pi/") - self.assertEqual(httputil.urljoin("/sn", "/pi"), "/sn/pi") - self.assertEqual(httputil.urljoin("/sn", "/"), "/sn/") - self.assertEqual(httputil.urljoin("/sn", ""), "/sn") - self.assertEqual(httputil.urljoin("/", "/pi/"), "/pi/") - self.assertEqual(httputil.urljoin("/", "/pi"), "/pi") - self.assertEqual(httputil.urljoin("/", "/"), "/") - self.assertEqual(httputil.urljoin("/", ""), "/") - self.assertEqual(httputil.urljoin("", "/pi/"), "/pi/") - self.assertEqual(httputil.urljoin("", "/pi"), "/pi") - self.assertEqual(httputil.urljoin("", "/"), "/") - self.assertEqual(httputil.urljoin("", ""), "/") - -if __name__ == '__main__': - unittest.main() diff --git a/libs/cherrypy/test/test_json.py b/libs/cherrypy/test/test_json.py deleted file mode 100644 index a02c076..0000000 --- a/libs/cherrypy/test/test_json.py +++ /dev/null @@ -1,79 +0,0 @@ -import cherrypy -from cherrypy.test import helper - -from cherrypy._cpcompat import json - -class JsonTest(helper.CPWebCase): - def setup_server(): - class Root(object): - def plain(self): - return 'hello' - plain.exposed = True - - def json_string(self): - return 'hello' - json_string.exposed = True - json_string._cp_config = {'tools.json_out.on': True} - - def json_list(self): - return ['a', 'b', 42] - json_list.exposed = True - json_list._cp_config = {'tools.json_out.on': True} - - def json_dict(self): - return {'answer': 42} - json_dict.exposed = True - json_dict._cp_config = {'tools.json_out.on': True} - - def json_post(self): - if cherrypy.request.json == [13, 'c']: - return 'ok' - else: - return 'nok' - json_post.exposed = True - json_post._cp_config = {'tools.json_in.on': True} - - root = Root() - cherrypy.tree.mount(root) - setup_server = staticmethod(setup_server) - - def test_json_output(self): - if json is None: - self.skip("json not found ") - return - - self.getPage("/plain") - self.assertBody("hello") - - self.getPage("/json_string") - self.assertBody('"hello"') - - self.getPage("/json_list") - self.assertBody('["a", "b", 42]') - - self.getPage("/json_dict") - self.assertBody('{"answer": 42}') - - def test_json_input(self): - if json is None: - self.skip("json not found ") - return - - body = '[13, "c"]' - headers = [('Content-Type', 'application/json'), - ('Content-Length', str(len(body)))] - self.getPage("/json_post", method="POST", headers=headers, body=body) - self.assertBody('ok') - - body = '[13, "c"]' - headers = [('Content-Type', 'text/plain'), - ('Content-Length', str(len(body)))] - self.getPage("/json_post", method="POST", headers=headers, body=body) - self.assertStatus(415, 'Expected an application/json content type') - - body = '[13, -]' - headers = [('Content-Type', 'application/json'), - ('Content-Length', str(len(body)))] - self.getPage("/json_post", method="POST", headers=headers, body=body) - self.assertStatus(400, 'Invalid JSON document') - diff --git a/libs/cherrypy/test/test_logging.py b/libs/cherrypy/test/test_logging.py deleted file mode 100644 index 7d506e8..0000000 --- a/libs/cherrypy/test/test_logging.py +++ /dev/null @@ -1,157 +0,0 @@ -"""Basic tests for the CherryPy core: request handling.""" - -import os -localDir = os.path.dirname(__file__) - -import cherrypy -from cherrypy._cpcompat import ntob, ntou, py3k - -access_log = os.path.join(localDir, "access.log") -error_log = os.path.join(localDir, "error.log") - -# Some unicode strings. -tartaros = ntou('\u03a4\u1f71\u03c1\u03c4\u03b1\u03c1\u03bf\u03c2', 'escape') -erebos = ntou('\u0388\u03c1\u03b5\u03b2\u03bf\u03c2.com', 'escape') - - -def setup_server(): - class Root: - - def index(self): - return "hello" - index.exposed = True - - def uni_code(self): - cherrypy.request.login = tartaros - cherrypy.request.remote.name = erebos - uni_code.exposed = True - - def slashes(self): - cherrypy.request.request_line = r'GET /slashed\path HTTP/1.1' - slashes.exposed = True - - def whitespace(self): - # User-Agent = "User-Agent" ":" 1*( product | comment ) - # comment = "(" *( ctext | quoted-pair | comment ) ")" - # ctext = - # TEXT = - # LWS = [CRLF] 1*( SP | HT ) - cherrypy.request.headers['User-Agent'] = 'Browzuh (1.0\r\n\t\t.3)' - whitespace.exposed = True - - def as_string(self): - return "content" - as_string.exposed = True - - def as_yield(self): - yield "content" - as_yield.exposed = True - - def error(self): - raise ValueError() - error.exposed = True - error._cp_config = {'tools.log_tracebacks.on': True} - - root = Root() - - - cherrypy.config.update({'log.error_file': error_log, - 'log.access_file': access_log, - }) - cherrypy.tree.mount(root) - - - -from cherrypy.test import helper, logtest - -class AccessLogTests(helper.CPWebCase, logtest.LogCase): - setup_server = staticmethod(setup_server) - - logfile = access_log - - def testNormalReturn(self): - self.markLog() - self.getPage("/as_string", - headers=[('Referer', 'http://www.cherrypy.org/'), - ('User-Agent', 'Mozilla/5.0')]) - self.assertBody('content') - self.assertStatus(200) - - intro = '%s - - [' % self.interface() - - self.assertLog(-1, intro) - - if [k for k, v in self.headers if k.lower() == 'content-length']: - self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 7 ' - '"http://www.cherrypy.org/" "Mozilla/5.0"' - % self.prefix()) - else: - self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 - ' - '"http://www.cherrypy.org/" "Mozilla/5.0"' - % self.prefix()) - - def testNormalYield(self): - self.markLog() - self.getPage("/as_yield") - self.assertBody('content') - self.assertStatus(200) - - intro = '%s - - [' % self.interface() - - self.assertLog(-1, intro) - if [k for k, v in self.headers if k.lower() == 'content-length']: - self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 7 "" ""' % - self.prefix()) - else: - self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 - "" ""' - % self.prefix()) - - def testEscapedOutput(self): - # Test unicode in access log pieces. - self.markLog() - self.getPage("/uni_code") - self.assertStatus(200) - if py3k: - # The repr of a bytestring in py3k includes a b'' prefix - self.assertLog(-1, repr(tartaros.encode('utf8'))[2:-1]) - else: - self.assertLog(-1, repr(tartaros.encode('utf8'))[1:-1]) - # Test the erebos value. Included inline for your enlightenment. - # Note the 'r' prefix--those backslashes are literals. - self.assertLog(-1, r'\xce\x88\xcf\x81\xce\xb5\xce\xb2\xce\xbf\xcf\x82') - - # Test backslashes in output. - self.markLog() - self.getPage("/slashes") - self.assertStatus(200) - if py3k: - self.assertLog(-1, ntob('"GET /slashed\\path HTTP/1.1"')) - else: - self.assertLog(-1, r'"GET /slashed\\path HTTP/1.1"') - - # Test whitespace in output. - self.markLog() - self.getPage("/whitespace") - self.assertStatus(200) - # Again, note the 'r' prefix. - self.assertLog(-1, r'"Browzuh (1.0\r\n\t\t.3)"') - - -class ErrorLogTests(helper.CPWebCase, logtest.LogCase): - setup_server = staticmethod(setup_server) - - logfile = error_log - - def testTracebacks(self): - # Test that tracebacks get written to the error log. - self.markLog() - ignore = helper.webtest.ignored_exceptions - ignore.append(ValueError) - try: - self.getPage("/error") - self.assertInBody("raise ValueError()") - self.assertLog(0, 'HTTP Traceback (most recent call last):') - self.assertLog(-3, 'raise ValueError()') - finally: - ignore.pop() - diff --git a/libs/cherrypy/test/test_mime.py b/libs/cherrypy/test/test_mime.py deleted file mode 100644 index 1605991..0000000 --- a/libs/cherrypy/test/test_mime.py +++ /dev/null @@ -1,128 +0,0 @@ -"""Tests for various MIME issues, including the safe_multipart Tool.""" - -import cherrypy -from cherrypy._cpcompat import ntob, ntou, sorted - -def setup_server(): - - class Root: - - def multipart(self, parts): - return repr(parts) - multipart.exposed = True - - def multipart_form_data(self, **kwargs): - return repr(list(sorted(kwargs.items()))) - multipart_form_data.exposed = True - - def flashupload(self, Filedata, Upload, Filename): - return ("Upload: %s, Filename: %s, Filedata: %r" % - (Upload, Filename, Filedata.file.read())) - flashupload.exposed = True - - cherrypy.config.update({'server.max_request_body_size': 0}) - cherrypy.tree.mount(Root()) - - -# Client-side code # - -from cherrypy.test import helper - -class MultipartTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_multipart(self): - text_part = ntou("This is the text version") - html_part = ntou(""" - - - - - - -This is the HTML version - - -""") - body = '\r\n'.join([ - "--123456789", - "Content-Type: text/plain; charset='ISO-8859-1'", - "Content-Transfer-Encoding: 7bit", - "", - text_part, - "--123456789", - "Content-Type: text/html; charset='ISO-8859-1'", - "", - html_part, - "--123456789--"]) - headers = [ - ('Content-Type', 'multipart/mixed; boundary=123456789'), - ('Content-Length', str(len(body))), - ] - self.getPage('/multipart', headers, "POST", body) - self.assertBody(repr([text_part, html_part])) - - def test_multipart_form_data(self): - body='\r\n'.join(['--X', - 'Content-Disposition: form-data; name="foo"', - '', - 'bar', - '--X', - # Test a param with more than one value. - # See http://www.cherrypy.org/ticket/1028 - 'Content-Disposition: form-data; name="baz"', - '', - '111', - '--X', - 'Content-Disposition: form-data; name="baz"', - '', - '333', - '--X--']) - self.getPage('/multipart_form_data', method='POST', - headers=[("Content-Type", "multipart/form-data;boundary=X"), - ("Content-Length", str(len(body))), - ], - body=body), - self.assertBody(repr([('baz', [ntou('111'), ntou('333')]), ('foo', ntou('bar'))])) - - -class SafeMultipartHandlingTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_Flash_Upload(self): - headers = [ - ('Accept', 'text/*'), - ('Content-Type', 'multipart/form-data; ' - 'boundary=----------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6'), - ('User-Agent', 'Shockwave Flash'), - ('Host', 'www.example.com:54583'), - ('Content-Length', '499'), - ('Connection', 'Keep-Alive'), - ('Cache-Control', 'no-cache'), - ] - filedata = ntob('\r\n' - '\r\n' - '\r\n') - body = (ntob( - '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n' - 'Content-Disposition: form-data; name="Filename"\r\n' - '\r\n' - '.project\r\n' - '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n' - 'Content-Disposition: form-data; ' - 'name="Filedata"; filename=".project"\r\n' - 'Content-Type: application/octet-stream\r\n' - '\r\n') - + filedata + - ntob('\r\n' - '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n' - 'Content-Disposition: form-data; name="Upload"\r\n' - '\r\n' - 'Submit Query\r\n' - # Flash apps omit the trailing \r\n on the last line: - '------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6--' - )) - self.getPage('/flashupload', headers, "POST", body) - self.assertBody("Upload: Submit Query, Filename: .project, " - "Filedata: %r" % filedata) - diff --git a/libs/cherrypy/test/test_misc_tools.py b/libs/cherrypy/test/test_misc_tools.py deleted file mode 100644 index 1dd1429..0000000 --- a/libs/cherrypy/test/test_misc_tools.py +++ /dev/null @@ -1,207 +0,0 @@ -import os -localDir = os.path.dirname(__file__) -logfile = os.path.join(localDir, "test_misc_tools.log") - -import cherrypy -from cherrypy import tools - - -def setup_server(): - class Root: - def index(self): - yield "Hello, world" - index.exposed = True - h = [("Content-Language", "en-GB"), ('Content-Type', 'text/plain')] - tools.response_headers(headers=h)(index) - - def other(self): - return "salut" - other.exposed = True - other._cp_config = { - 'tools.response_headers.on': True, - 'tools.response_headers.headers': [("Content-Language", "fr"), - ('Content-Type', 'text/plain')], - 'tools.log_hooks.on': True, - } - - - class Accept: - _cp_config = {'tools.accept.on': True} - - def index(self): - return 'Atom feed' - index.exposed = True - - # In Python 2.4+, we could use a decorator instead: - # @tools.accept('application/atom+xml') - def feed(self): - return """ - - Unknown Blog -""" - feed.exposed = True - feed._cp_config = {'tools.accept.media': 'application/atom+xml'} - - def select(self): - # We could also write this: mtype = cherrypy.lib.accept.accept(...) - mtype = tools.accept.callable(['text/html', 'text/plain']) - if mtype == 'text/html': - return "

Page Title

" - else: - return "PAGE TITLE" - select.exposed = True - - class Referer: - def accept(self): - return "Accepted!" - accept.exposed = True - reject = accept - - class AutoVary: - def index(self): - # Read a header directly with 'get' - ae = cherrypy.request.headers.get('Accept-Encoding') - # Read a header directly with '__getitem__' - cl = cherrypy.request.headers['Host'] - # Read a header directly with '__contains__' - hasif = 'If-Modified-Since' in cherrypy.request.headers - # Read a header directly with 'has_key' - if hasattr(dict, 'has_key'): - # Python 2 - has = cherrypy.request.headers.has_key('Range') - else: - # Python 3 - has = 'Range' in cherrypy.request.headers - # Call a lib function - mtype = tools.accept.callable(['text/html', 'text/plain']) - return "Hello, world!" - index.exposed = True - - conf = {'/referer': {'tools.referer.on': True, - 'tools.referer.pattern': r'http://[^/]*example\.com', - }, - '/referer/reject': {'tools.referer.accept': False, - 'tools.referer.accept_missing': True, - }, - '/autovary': {'tools.autovary.on': True}, - } - - root = Root() - root.referer = Referer() - root.accept = Accept() - root.autovary = AutoVary() - cherrypy.tree.mount(root, config=conf) - cherrypy.config.update({'log.error_file': logfile}) - - -from cherrypy.test import helper - -class ResponseHeadersTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def testResponseHeadersDecorator(self): - self.getPage('/') - self.assertHeader("Content-Language", "en-GB") - self.assertHeader('Content-Type', 'text/plain;charset=utf-8') - - def testResponseHeaders(self): - self.getPage('/other') - self.assertHeader("Content-Language", "fr") - self.assertHeader('Content-Type', 'text/plain;charset=utf-8') - - -class RefererTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def testReferer(self): - self.getPage('/referer/accept') - self.assertErrorPage(403, 'Forbidden Referer header.') - - self.getPage('/referer/accept', - headers=[('Referer', 'http://www.example.com/')]) - self.assertStatus(200) - self.assertBody('Accepted!') - - # Reject - self.getPage('/referer/reject') - self.assertStatus(200) - self.assertBody('Accepted!') - - self.getPage('/referer/reject', - headers=[('Referer', 'http://www.example.com/')]) - self.assertErrorPage(403, 'Forbidden Referer header.') - - -class AcceptTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_Accept_Tool(self): - # Test with no header provided - self.getPage('/accept/feed') - self.assertStatus(200) - self.assertInBody('Unknown Blog') - - # Specify exact media type - self.getPage('/accept/feed', headers=[('Accept', 'application/atom+xml')]) - self.assertStatus(200) - self.assertInBody('Unknown Blog') - - # Specify matching media range - self.getPage('/accept/feed', headers=[('Accept', 'application/*')]) - self.assertStatus(200) - self.assertInBody('Unknown Blog') - - # Specify all media ranges - self.getPage('/accept/feed', headers=[('Accept', '*/*')]) - self.assertStatus(200) - self.assertInBody('Unknown Blog') - - # Specify unacceptable media types - self.getPage('/accept/feed', headers=[('Accept', 'text/html')]) - self.assertErrorPage(406, - "Your client sent this Accept header: text/html. " - "But this resource only emits these media types: " - "application/atom+xml.") - - # Test resource where tool is 'on' but media is None (not set). - self.getPage('/accept/') - self.assertStatus(200) - self.assertBody('Atom feed') - - def test_accept_selection(self): - # Try both our expected media types - self.getPage('/accept/select', [('Accept', 'text/html')]) - self.assertStatus(200) - self.assertBody('

Page Title

') - self.getPage('/accept/select', [('Accept', 'text/plain')]) - self.assertStatus(200) - self.assertBody('PAGE TITLE') - self.getPage('/accept/select', [('Accept', 'text/plain, text/*;q=0.5')]) - self.assertStatus(200) - self.assertBody('PAGE TITLE') - - # text/* and */* should prefer text/html since it comes first - # in our 'media' argument to tools.accept - self.getPage('/accept/select', [('Accept', 'text/*')]) - self.assertStatus(200) - self.assertBody('

Page Title

') - self.getPage('/accept/select', [('Accept', '*/*')]) - self.assertStatus(200) - self.assertBody('

Page Title

') - - # Try unacceptable media types - self.getPage('/accept/select', [('Accept', 'application/xml')]) - self.assertErrorPage(406, - "Your client sent this Accept header: application/xml. " - "But this resource only emits these media types: " - "text/html, text/plain.") - - -class AutoVaryTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def testAutoVary(self): - self.getPage('/autovary/') - self.assertHeader( - "Vary", 'Accept, Accept-Charset, Accept-Encoding, Host, If-Modified-Since, Range') - diff --git a/libs/cherrypy/test/test_objectmapping.py b/libs/cherrypy/test/test_objectmapping.py deleted file mode 100644 index 8dcf2d3..0000000 --- a/libs/cherrypy/test/test_objectmapping.py +++ /dev/null @@ -1,404 +0,0 @@ -import cherrypy -from cherrypy._cpcompat import ntou -from cherrypy._cptree import Application -from cherrypy.test import helper - -script_names = ["", "/foo", "/users/fred/blog", "/corp/blog"] - - -class ObjectMappingTest(helper.CPWebCase): - - def setup_server(): - class Root: - def index(self, name="world"): - return name - index.exposed = True - - def foobar(self): - return "bar" - foobar.exposed = True - - def default(self, *params, **kwargs): - return "default:" + repr(params) - default.exposed = True - - def other(self): - return "other" - other.exposed = True - - def extra(self, *p): - return repr(p) - extra.exposed = True - - def redirect(self): - raise cherrypy.HTTPRedirect('dir1/', 302) - redirect.exposed = True - - def notExposed(self): - return "not exposed" - - def confvalue(self): - return cherrypy.request.config.get("user") - confvalue.exposed = True - - def redirect_via_url(self, path): - raise cherrypy.HTTPRedirect(cherrypy.url(path)) - redirect_via_url.exposed = True - - def translate_html(self): - return "OK" - translate_html.exposed = True - - def mapped_func(self, ID=None): - return "ID is %s" % ID - mapped_func.exposed = True - setattr(Root, "Von B\xfclow", mapped_func) - - - class Exposing: - def base(self): - return "expose works!" - cherrypy.expose(base) - cherrypy.expose(base, "1") - cherrypy.expose(base, "2") - - class ExposingNewStyle(object): - def base(self): - return "expose works!" - cherrypy.expose(base) - cherrypy.expose(base, "1") - cherrypy.expose(base, "2") - - - class Dir1: - def index(self): - return "index for dir1" - index.exposed = True - - def myMethod(self): - return "myMethod from dir1, path_info is:" + repr(cherrypy.request.path_info) - myMethod.exposed = True - myMethod._cp_config = {'tools.trailing_slash.extra': True} - - def default(self, *params): - return "default for dir1, param is:" + repr(params) - default.exposed = True - - - class Dir2: - def index(self): - return "index for dir2, path is:" + cherrypy.request.path_info - index.exposed = True - - def script_name(self): - return cherrypy.tree.script_name() - script_name.exposed = True - - def cherrypy_url(self): - return cherrypy.url("/extra") - cherrypy_url.exposed = True - - def posparam(self, *vpath): - return "/".join(vpath) - posparam.exposed = True - - - class Dir3: - def default(self): - return "default for dir3, not exposed" - - class Dir4: - def index(self): - return "index for dir4, not exposed" - - class DefNoIndex: - def default(self, *args): - raise cherrypy.HTTPRedirect("contact") - default.exposed = True - - # MethodDispatcher code - class ByMethod: - exposed = True - - def __init__(self, *things): - self.things = list(things) - - def GET(self): - return repr(self.things) - - def POST(self, thing): - self.things.append(thing) - - class Collection: - default = ByMethod('a', 'bit') - - Root.exposing = Exposing() - Root.exposingnew = ExposingNewStyle() - Root.dir1 = Dir1() - Root.dir1.dir2 = Dir2() - Root.dir1.dir2.dir3 = Dir3() - Root.dir1.dir2.dir3.dir4 = Dir4() - Root.defnoindex = DefNoIndex() - Root.bymethod = ByMethod('another') - Root.collection = Collection() - - d = cherrypy.dispatch.MethodDispatcher() - for url in script_names: - conf = {'/': {'user': (url or "/").split("/")[-2]}, - '/bymethod': {'request.dispatch': d}, - '/collection': {'request.dispatch': d}, - } - cherrypy.tree.mount(Root(), url, conf) - - - class Isolated: - def index(self): - return "made it!" - index.exposed = True - - cherrypy.tree.mount(Isolated(), "/isolated") - - class AnotherApp: - - exposed = True - - def GET(self): - return "milk" - - cherrypy.tree.mount(AnotherApp(), "/app", {'/': {'request.dispatch': d}}) - setup_server = staticmethod(setup_server) - - - def testObjectMapping(self): - for url in script_names: - prefix = self.script_name = url - - self.getPage('/') - self.assertBody('world') - - self.getPage("/dir1/myMethod") - self.assertBody("myMethod from dir1, path_info is:'/dir1/myMethod'") - - self.getPage("/this/method/does/not/exist") - self.assertBody("default:('this', 'method', 'does', 'not', 'exist')") - - self.getPage("/extra/too/much") - self.assertBody("('too', 'much')") - - self.getPage("/other") - self.assertBody('other') - - self.getPage("/notExposed") - self.assertBody("default:('notExposed',)") - - self.getPage("/dir1/dir2/") - self.assertBody('index for dir2, path is:/dir1/dir2/') - - # Test omitted trailing slash (should be redirected by default). - self.getPage("/dir1/dir2") - self.assertStatus(301) - self.assertHeader('Location', '%s/dir1/dir2/' % self.base()) - - # Test extra trailing slash (should be redirected if configured). - self.getPage("/dir1/myMethod/") - self.assertStatus(301) - self.assertHeader('Location', '%s/dir1/myMethod' % self.base()) - - # Test that default method must be exposed in order to match. - self.getPage("/dir1/dir2/dir3/dir4/index") - self.assertBody("default for dir1, param is:('dir2', 'dir3', 'dir4', 'index')") - - # Test *vpath when default() is defined but not index() - # This also tests HTTPRedirect with default. - self.getPage("/defnoindex") - self.assertStatus((302, 303)) - self.assertHeader('Location', '%s/contact' % self.base()) - self.getPage("/defnoindex/") - self.assertStatus((302, 303)) - self.assertHeader('Location', '%s/defnoindex/contact' % self.base()) - self.getPage("/defnoindex/page") - self.assertStatus((302, 303)) - self.assertHeader('Location', '%s/defnoindex/contact' % self.base()) - - self.getPage("/redirect") - self.assertStatus('302 Found') - self.assertHeader('Location', '%s/dir1/' % self.base()) - - if not getattr(cherrypy.server, "using_apache", False): - # Test that we can use URL's which aren't all valid Python identifiers - # This should also test the %XX-unquoting of URL's. - self.getPage("/Von%20B%fclow?ID=14") - self.assertBody("ID is 14") - - # Test that %2F in the path doesn't get unquoted too early; - # that is, it should not be used to separate path components. - # See ticket #393. - self.getPage("/page%2Fname") - self.assertBody("default:('page/name',)") - - self.getPage("/dir1/dir2/script_name") - self.assertBody(url) - self.getPage("/dir1/dir2/cherrypy_url") - self.assertBody("%s/extra" % self.base()) - - # Test that configs don't overwrite each other from diferent apps - self.getPage("/confvalue") - self.assertBody((url or "/").split("/")[-2]) - - self.script_name = "" - - # Test absoluteURI's in the Request-Line - self.getPage('http://%s:%s/' % (self.interface(), self.PORT)) - self.assertBody('world') - - self.getPage('http://%s:%s/abs/?service=http://192.168.0.1/x/y/z' % - (self.interface(), self.PORT)) - self.assertBody("default:('abs',)") - - self.getPage('/rel/?service=http://192.168.120.121:8000/x/y/z') - self.assertBody("default:('rel',)") - - # Test that the "isolated" app doesn't leak url's into the root app. - # If it did leak, Root.default() would answer with - # "default:('isolated', 'doesnt', 'exist')". - self.getPage("/isolated/") - self.assertStatus("200 OK") - self.assertBody("made it!") - self.getPage("/isolated/doesnt/exist") - self.assertStatus("404 Not Found") - - # Make sure /foobar maps to Root.foobar and not to the app - # mounted at /foo. See http://www.cherrypy.org/ticket/573 - self.getPage("/foobar") - self.assertBody("bar") - - def test_translate(self): - self.getPage("/translate_html") - self.assertStatus("200 OK") - self.assertBody("OK") - - self.getPage("/translate.html") - self.assertStatus("200 OK") - self.assertBody("OK") - - self.getPage("/translate-html") - self.assertStatus("200 OK") - self.assertBody("OK") - - def test_redir_using_url(self): - for url in script_names: - prefix = self.script_name = url - - # Test the absolute path to the parent (leading slash) - self.getPage('/redirect_via_url?path=./') - self.assertStatus(('302 Found', '303 See Other')) - self.assertHeader('Location', '%s/' % self.base()) - - # Test the relative path to the parent (no leading slash) - self.getPage('/redirect_via_url?path=./') - self.assertStatus(('302 Found', '303 See Other')) - self.assertHeader('Location', '%s/' % self.base()) - - # Test the absolute path to the parent (leading slash) - self.getPage('/redirect_via_url/?path=./') - self.assertStatus(('302 Found', '303 See Other')) - self.assertHeader('Location', '%s/' % self.base()) - - # Test the relative path to the parent (no leading slash) - self.getPage('/redirect_via_url/?path=./') - self.assertStatus(('302 Found', '303 See Other')) - self.assertHeader('Location', '%s/' % self.base()) - - def testPositionalParams(self): - self.getPage("/dir1/dir2/posparam/18/24/hut/hike") - self.assertBody("18/24/hut/hike") - - # intermediate index methods should not receive posparams; - # only the "final" index method should do so. - self.getPage("/dir1/dir2/5/3/sir") - self.assertBody("default for dir1, param is:('dir2', '5', '3', 'sir')") - - # test that extra positional args raises an 404 Not Found - # See http://www.cherrypy.org/ticket/733. - self.getPage("/dir1/dir2/script_name/extra/stuff") - self.assertStatus(404) - - def testExpose(self): - # Test the cherrypy.expose function/decorator - self.getPage("/exposing/base") - self.assertBody("expose works!") - - self.getPage("/exposing/1") - self.assertBody("expose works!") - - self.getPage("/exposing/2") - self.assertBody("expose works!") - - self.getPage("/exposingnew/base") - self.assertBody("expose works!") - - self.getPage("/exposingnew/1") - self.assertBody("expose works!") - - self.getPage("/exposingnew/2") - self.assertBody("expose works!") - - def testMethodDispatch(self): - self.getPage("/bymethod") - self.assertBody("['another']") - self.assertHeader('Allow', 'GET, HEAD, POST') - - self.getPage("/bymethod", method="HEAD") - self.assertBody("") - self.assertHeader('Allow', 'GET, HEAD, POST') - - self.getPage("/bymethod", method="POST", body="thing=one") - self.assertBody("") - self.assertHeader('Allow', 'GET, HEAD, POST') - - self.getPage("/bymethod") - self.assertBody(repr(['another', ntou('one')])) - self.assertHeader('Allow', 'GET, HEAD, POST') - - self.getPage("/bymethod", method="PUT") - self.assertErrorPage(405) - self.assertHeader('Allow', 'GET, HEAD, POST') - - # Test default with posparams - self.getPage("/collection/silly", method="POST") - self.getPage("/collection", method="GET") - self.assertBody("['a', 'bit', 'silly']") - - # Test custom dispatcher set on app root (see #737). - self.getPage("/app") - self.assertBody("milk") - - def testTreeMounting(self): - class Root(object): - def hello(self): - return "Hello world!" - hello.exposed = True - - # When mounting an application instance, - # we can't specify a different script name in the call to mount. - a = Application(Root(), '/somewhere') - self.assertRaises(ValueError, cherrypy.tree.mount, a, '/somewhereelse') - - # When mounting an application instance... - a = Application(Root(), '/somewhere') - # ...we MUST allow in identical script name in the call to mount... - cherrypy.tree.mount(a, '/somewhere') - self.getPage('/somewhere/hello') - self.assertStatus(200) - # ...and MUST allow a missing script_name. - del cherrypy.tree.apps['/somewhere'] - cherrypy.tree.mount(a) - self.getPage('/somewhere/hello') - self.assertStatus(200) - - # In addition, we MUST be able to create an Application using - # script_name == None for access to the wsgi_environ. - a = Application(Root(), script_name=None) - # However, this does not apply to tree.mount - self.assertRaises(TypeError, cherrypy.tree.mount, a, None) - diff --git a/libs/cherrypy/test/test_proxy.py b/libs/cherrypy/test/test_proxy.py deleted file mode 100644 index 2fbb619..0000000 --- a/libs/cherrypy/test/test_proxy.py +++ /dev/null @@ -1,129 +0,0 @@ -import cherrypy -from cherrypy.test import helper - -script_names = ["", "/path/to/myapp"] - - -class ProxyTest(helper.CPWebCase): - - def setup_server(): - - # Set up site - cherrypy.config.update({ - 'tools.proxy.on': True, - 'tools.proxy.base': 'www.mydomain.test', - }) - - # Set up application - - class Root: - - def __init__(self, sn): - # Calculate a URL outside of any requests. - self.thisnewpage = cherrypy.url("/this/new/page", script_name=sn) - - def pageurl(self): - return self.thisnewpage - pageurl.exposed = True - - def index(self): - raise cherrypy.HTTPRedirect('dummy') - index.exposed = True - - def remoteip(self): - return cherrypy.request.remote.ip - remoteip.exposed = True - - def xhost(self): - raise cherrypy.HTTPRedirect('blah') - xhost.exposed = True - xhost._cp_config = {'tools.proxy.local': 'X-Host', - 'tools.trailing_slash.extra': True, - } - - def base(self): - return cherrypy.request.base - base.exposed = True - - def ssl(self): - return cherrypy.request.base - ssl.exposed = True - ssl._cp_config = {'tools.proxy.scheme': 'X-Forwarded-Ssl'} - - def newurl(self): - return ("Browse to this page." - % cherrypy.url("/this/new/page")) - newurl.exposed = True - - for sn in script_names: - cherrypy.tree.mount(Root(sn), sn) - setup_server = staticmethod(setup_server) - - def testProxy(self): - self.getPage("/") - self.assertHeader('Location', - "%s://www.mydomain.test%s/dummy" % - (self.scheme, self.prefix())) - - # Test X-Forwarded-Host (Apache 1.3.33+ and Apache 2) - self.getPage("/", headers=[('X-Forwarded-Host', 'http://www.example.test')]) - self.assertHeader('Location', "http://www.example.test/dummy") - self.getPage("/", headers=[('X-Forwarded-Host', 'www.example.test')]) - self.assertHeader('Location', "%s://www.example.test/dummy" % self.scheme) - # Test multiple X-Forwarded-Host headers - self.getPage("/", headers=[ - ('X-Forwarded-Host', 'http://www.example.test, www.cherrypy.test'), - ]) - self.assertHeader('Location', "http://www.example.test/dummy") - - # Test X-Forwarded-For (Apache2) - self.getPage("/remoteip", - headers=[('X-Forwarded-For', '192.168.0.20')]) - self.assertBody("192.168.0.20") - self.getPage("/remoteip", - headers=[('X-Forwarded-For', '67.15.36.43, 192.168.0.20')]) - self.assertBody("192.168.0.20") - - # Test X-Host (lighttpd; see https://trac.lighttpd.net/trac/ticket/418) - self.getPage("/xhost", headers=[('X-Host', 'www.example.test')]) - self.assertHeader('Location', "%s://www.example.test/blah" % self.scheme) - - # Test X-Forwarded-Proto (lighttpd) - self.getPage("/base", headers=[('X-Forwarded-Proto', 'https')]) - self.assertBody("https://www.mydomain.test") - - # Test X-Forwarded-Ssl (webfaction?) - self.getPage("/ssl", headers=[('X-Forwarded-Ssl', 'on')]) - self.assertBody("https://www.mydomain.test") - - # Test cherrypy.url() - for sn in script_names: - # Test the value inside requests - self.getPage(sn + "/newurl") - self.assertBody("Browse to this page.") - self.getPage(sn + "/newurl", headers=[('X-Forwarded-Host', - 'http://www.example.test')]) - self.assertBody("Browse to this page.") - - # Test the value outside requests - port = "" - if self.scheme == "http" and self.PORT != 80: - port = ":%s" % self.PORT - elif self.scheme == "https" and self.PORT != 443: - port = ":%s" % self.PORT - host = self.HOST - if host in ('0.0.0.0', '::'): - import socket - host = socket.gethostname() - expected = ("%s://%s%s%s/this/new/page" - % (self.scheme, host, port, sn)) - self.getPage(sn + "/pageurl") - self.assertBody(expected) - - # Test trailing slash (see http://www.cherrypy.org/ticket/562). - self.getPage("/xhost/", headers=[('X-Host', 'www.example.test')]) - self.assertHeader('Location', "%s://www.example.test/xhost" - % self.scheme) - diff --git a/libs/cherrypy/test/test_refleaks.py b/libs/cherrypy/test/test_refleaks.py deleted file mode 100644 index 279935e..0000000 --- a/libs/cherrypy/test/test_refleaks.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Tests for refleaks.""" - -from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob -import threading - -import cherrypy - - -data = object() - - -from cherrypy.test import helper - - -class ReferenceTests(helper.CPWebCase): - - def setup_server(): - - class Root: - def index(self, *args, **kwargs): - cherrypy.request.thing = data - return "Hello world!" - index.exposed = True - - cherrypy.tree.mount(Root()) - setup_server = staticmethod(setup_server) - - def test_threadlocal_garbage(self): - success = [] - - def getpage(): - host = '%s:%s' % (self.interface(), self.PORT) - if self.scheme == 'https': - c = HTTPSConnection(host) - else: - c = HTTPConnection(host) - try: - c.putrequest('GET', '/') - c.endheaders() - response = c.getresponse() - body = response.read() - self.assertEqual(response.status, 200) - self.assertEqual(body, ntob("Hello world!")) - finally: - c.close() - success.append(True) - - ITERATIONS = 25 - ts = [] - for _ in range(ITERATIONS): - t = threading.Thread(target=getpage) - ts.append(t) - t.start() - - for t in ts: - t.join() - - self.assertEqual(len(success), ITERATIONS) - diff --git a/libs/cherrypy/test/test_request_obj.py b/libs/cherrypy/test/test_request_obj.py deleted file mode 100644 index 26eea56..0000000 --- a/libs/cherrypy/test/test_request_obj.py +++ /dev/null @@ -1,737 +0,0 @@ -"""Basic tests for the cherrypy.Request object.""" - -import os -localDir = os.path.dirname(__file__) -import sys -import types -from cherrypy._cpcompat import IncompleteRead, ntob, ntou, unicodestr - -import cherrypy -from cherrypy import _cptools, tools -from cherrypy.lib import httputil - -defined_http_methods = ("OPTIONS", "GET", "HEAD", "POST", "PUT", "DELETE", - "TRACE", "PROPFIND") - - -# Client-side code # - -from cherrypy.test import helper - -class RequestObjectTests(helper.CPWebCase): - - def setup_server(): - class Root: - - def index(self): - return "hello" - index.exposed = True - - def scheme(self): - return cherrypy.request.scheme - scheme.exposed = True - - root = Root() - - - class TestType(type): - """Metaclass which automatically exposes all functions in each subclass, - and adds an instance of the subclass as an attribute of root. - """ - def __init__(cls, name, bases, dct): - type.__init__(cls, name, bases, dct) - for value in dct.values(): - if isinstance(value, types.FunctionType): - value.exposed = True - setattr(root, name.lower(), cls()) - Test = TestType('Test', (object,), {}) - - class PathInfo(Test): - - def default(self, *args): - return cherrypy.request.path_info - - class Params(Test): - - def index(self, thing): - return repr(thing) - - def ismap(self, x, y): - return "Coordinates: %s, %s" % (x, y) - - def default(self, *args, **kwargs): - return "args: %s kwargs: %s" % (args, kwargs) - default._cp_config = {'request.query_string_encoding': 'latin1'} - - - class ParamErrorsCallable(object): - exposed = True - def __call__(self): - return "data" - - class ParamErrors(Test): - - def one_positional(self, param1): - return "data" - one_positional.exposed = True - - def one_positional_args(self, param1, *args): - return "data" - one_positional_args.exposed = True - - def one_positional_args_kwargs(self, param1, *args, **kwargs): - return "data" - one_positional_args_kwargs.exposed = True - - def one_positional_kwargs(self, param1, **kwargs): - return "data" - one_positional_kwargs.exposed = True - - def no_positional(self): - return "data" - no_positional.exposed = True - - def no_positional_args(self, *args): - return "data" - no_positional_args.exposed = True - - def no_positional_args_kwargs(self, *args, **kwargs): - return "data" - no_positional_args_kwargs.exposed = True - - def no_positional_kwargs(self, **kwargs): - return "data" - no_positional_kwargs.exposed = True - - callable_object = ParamErrorsCallable() - - def raise_type_error(self, **kwargs): - raise TypeError("Client Error") - raise_type_error.exposed = True - - def raise_type_error_with_default_param(self, x, y=None): - return '%d' % 'a' # throw an exception - raise_type_error_with_default_param.exposed = True - - def callable_error_page(status, **kwargs): - return "Error %s - Well, I'm very sorry but you haven't paid!" % status - - - class Error(Test): - - _cp_config = {'tools.log_tracebacks.on': True, - } - - def reason_phrase(self): - raise cherrypy.HTTPError("410 Gone fishin'") - - def custom(self, err='404'): - raise cherrypy.HTTPError(int(err), "No, really, not found!") - custom._cp_config = {'error_page.404': os.path.join(localDir, "static/index.html"), - 'error_page.401': callable_error_page, - } - - def custom_default(self): - return 1 + 'a' # raise an unexpected error - custom_default._cp_config = {'error_page.default': callable_error_page} - - def noexist(self): - raise cherrypy.HTTPError(404, "No, really, not found!") - noexist._cp_config = {'error_page.404': "nonexistent.html"} - - def page_method(self): - raise ValueError() - - def page_yield(self): - yield "howdy" - raise ValueError() - - def page_streamed(self): - yield "word up" - raise ValueError() - yield "very oops" - page_streamed._cp_config = {"response.stream": True} - - def cause_err_in_finalize(self): - # Since status must start with an int, this should error. - cherrypy.response.status = "ZOO OK" - cause_err_in_finalize._cp_config = {'request.show_tracebacks': False} - - def rethrow(self): - """Test that an error raised here will be thrown out to the server.""" - raise ValueError() - rethrow._cp_config = {'request.throw_errors': True} - - - class Expect(Test): - - def expectation_failed(self): - expect = cherrypy.request.headers.elements("Expect") - if expect and expect[0].value != '100-continue': - raise cherrypy.HTTPError(400) - raise cherrypy.HTTPError(417, 'Expectation Failed') - - class Headers(Test): - - def default(self, headername): - """Spit back out the value for the requested header.""" - return cherrypy.request.headers[headername] - - def doubledheaders(self): - # From http://www.cherrypy.org/ticket/165: - # "header field names should not be case sensitive sayes the rfc. - # if i set a headerfield in complete lowercase i end up with two - # header fields, one in lowercase, the other in mixed-case." - - # Set the most common headers - hMap = cherrypy.response.headers - hMap['content-type'] = "text/html" - hMap['content-length'] = 18 - hMap['server'] = 'CherryPy headertest' - hMap['location'] = ('%s://%s:%s/headers/' - % (cherrypy.request.local.ip, - cherrypy.request.local.port, - cherrypy.request.scheme)) - - # Set a rare header for fun - hMap['Expires'] = 'Thu, 01 Dec 2194 16:00:00 GMT' - - return "double header test" - - def ifmatch(self): - val = cherrypy.request.headers['If-Match'] - assert isinstance(val, unicodestr) - cherrypy.response.headers['ETag'] = val - return val - - - class HeaderElements(Test): - - def get_elements(self, headername): - e = cherrypy.request.headers.elements(headername) - return "\n".join([unicodestr(x) for x in e]) - - - class Method(Test): - - def index(self): - m = cherrypy.request.method - if m in defined_http_methods or m == "CONNECT": - return m - - if m == "LINK": - raise cherrypy.HTTPError(405) - else: - raise cherrypy.HTTPError(501) - - def parameterized(self, data): - return data - - def request_body(self): - # This should be a file object (temp file), - # which CP will just pipe back out if we tell it to. - return cherrypy.request.body - - def reachable(self): - return "success" - - class Divorce: - """HTTP Method handlers shouldn't collide with normal method names. - For example, a GET-handler shouldn't collide with a method named 'get'. - - If you build HTTP method dispatching into CherryPy, rewrite this class - to use your new dispatch mechanism and make sure that: - "GET /divorce HTTP/1.1" maps to divorce.index() and - "GET /divorce/get?ID=13 HTTP/1.1" maps to divorce.get() - """ - - documents = {} - - def index(self): - yield "

Choose your document

\n" - yield "
    \n" - for id, contents in self.documents.items(): - yield ("
  • %s: %s
  • \n" - % (id, id, contents)) - yield "
" - index.exposed = True - - def get(self, ID): - return ("Divorce document %s: %s" % - (ID, self.documents.get(ID, "empty"))) - get.exposed = True - - root.divorce = Divorce() - - - class ThreadLocal(Test): - - def index(self): - existing = repr(getattr(cherrypy.request, "asdf", None)) - cherrypy.request.asdf = "rassfrassin" - return existing - - appconf = { - '/method': {'request.methods_with_bodies': ("POST", "PUT", "PROPFIND")}, - } - cherrypy.tree.mount(root, config=appconf) - setup_server = staticmethod(setup_server) - - def test_scheme(self): - self.getPage("/scheme") - self.assertBody(self.scheme) - - def testRelativeURIPathInfo(self): - self.getPage("/pathinfo/foo/bar") - self.assertBody("/pathinfo/foo/bar") - - def testAbsoluteURIPathInfo(self): - # http://cherrypy.org/ticket/1061 - self.getPage("http://localhost/pathinfo/foo/bar") - self.assertBody("/pathinfo/foo/bar") - - def testParams(self): - self.getPage("/params/?thing=a") - self.assertBody(repr(ntou("a"))) - - self.getPage("/params/?thing=a&thing=b&thing=c") - self.assertBody(repr([ntou('a'), ntou('b'), ntou('c')])) - - # Test friendly error message when given params are not accepted. - cherrypy.config.update({"request.show_mismatched_params": True}) - self.getPage("/params/?notathing=meeting") - self.assertInBody("Missing parameters: thing") - self.getPage("/params/?thing=meeting¬athing=meeting") - self.assertInBody("Unexpected query string parameters: notathing") - - # Test ability to turn off friendly error messages - cherrypy.config.update({"request.show_mismatched_params": False}) - self.getPage("/params/?notathing=meeting") - self.assertInBody("Not Found") - self.getPage("/params/?thing=meeting¬athing=meeting") - self.assertInBody("Not Found") - - # Test "% HEX HEX"-encoded URL, param keys, and values - self.getPage("/params/%d4%20%e3/cheese?Gruy%E8re=Bulgn%e9ville") - self.assertBody("args: %s kwargs: %s" % - (('\xd4 \xe3', 'cheese'), - {'Gruy\xe8re': ntou('Bulgn\xe9ville')})) - - # Make sure that encoded = and & get parsed correctly - self.getPage("/params/code?url=http%3A//cherrypy.org/index%3Fa%3D1%26b%3D2") - self.assertBody("args: %s kwargs: %s" % - (('code',), - {'url': ntou('http://cherrypy.org/index?a=1&b=2')})) - - # Test coordinates sent by - self.getPage("/params/ismap?223,114") - self.assertBody("Coordinates: 223, 114") - - # Test "name[key]" dict-like params - self.getPage("/params/dictlike?a[1]=1&a[2]=2&b=foo&b[bar]=baz") - self.assertBody("args: %s kwargs: %s" % - (('dictlike',), - {'a[1]': ntou('1'), 'b[bar]': ntou('baz'), - 'b': ntou('foo'), 'a[2]': ntou('2')})) - - def testParamErrors(self): - - # test that all of the handlers work when given - # the correct parameters in order to ensure that the - # errors below aren't coming from some other source. - for uri in ( - '/paramerrors/one_positional?param1=foo', - '/paramerrors/one_positional_args?param1=foo', - '/paramerrors/one_positional_args/foo', - '/paramerrors/one_positional_args/foo/bar/baz', - '/paramerrors/one_positional_args_kwargs?param1=foo¶m2=bar', - '/paramerrors/one_positional_args_kwargs/foo?param2=bar¶m3=baz', - '/paramerrors/one_positional_args_kwargs/foo/bar/baz?param2=bar¶m3=baz', - '/paramerrors/one_positional_kwargs?param1=foo¶m2=bar¶m3=baz', - '/paramerrors/one_positional_kwargs/foo?param4=foo¶m2=bar¶m3=baz', - '/paramerrors/no_positional', - '/paramerrors/no_positional_args/foo', - '/paramerrors/no_positional_args/foo/bar/baz', - '/paramerrors/no_positional_args_kwargs?param1=foo¶m2=bar', - '/paramerrors/no_positional_args_kwargs/foo?param2=bar', - '/paramerrors/no_positional_args_kwargs/foo/bar/baz?param2=bar¶m3=baz', - '/paramerrors/no_positional_kwargs?param1=foo¶m2=bar', - '/paramerrors/callable_object', - ): - self.getPage(uri) - self.assertStatus(200) - - # query string parameters are part of the URI, so if they are wrong - # for a particular handler, the status MUST be a 404. - error_msgs = [ - 'Missing parameters', - 'Nothing matches the given URI', - 'Multiple values for parameters', - 'Unexpected query string parameters', - 'Unexpected body parameters', - ] - for uri, msg in ( - ('/paramerrors/one_positional', error_msgs[0]), - ('/paramerrors/one_positional?foo=foo', error_msgs[0]), - ('/paramerrors/one_positional/foo/bar/baz', error_msgs[1]), - ('/paramerrors/one_positional/foo?param1=foo', error_msgs[2]), - ('/paramerrors/one_positional/foo?param1=foo¶m2=foo', error_msgs[2]), - ('/paramerrors/one_positional_args/foo?param1=foo¶m2=foo', error_msgs[2]), - ('/paramerrors/one_positional_args/foo/bar/baz?param2=foo', error_msgs[3]), - ('/paramerrors/one_positional_args_kwargs/foo/bar/baz?param1=bar¶m3=baz', error_msgs[2]), - ('/paramerrors/one_positional_kwargs/foo?param1=foo¶m2=bar¶m3=baz', error_msgs[2]), - ('/paramerrors/no_positional/boo', error_msgs[1]), - ('/paramerrors/no_positional?param1=foo', error_msgs[3]), - ('/paramerrors/no_positional_args/boo?param1=foo', error_msgs[3]), - ('/paramerrors/no_positional_kwargs/boo?param1=foo', error_msgs[1]), - ('/paramerrors/callable_object?param1=foo', error_msgs[3]), - ('/paramerrors/callable_object/boo', error_msgs[1]), - ): - for show_mismatched_params in (True, False): - cherrypy.config.update({'request.show_mismatched_params': show_mismatched_params}) - self.getPage(uri) - self.assertStatus(404) - if show_mismatched_params: - self.assertInBody(msg) - else: - self.assertInBody("Not Found") - - # if body parameters are wrong, a 400 must be returned. - for uri, body, msg in ( - ('/paramerrors/one_positional/foo', 'param1=foo', error_msgs[2]), - ('/paramerrors/one_positional/foo', 'param1=foo¶m2=foo', error_msgs[2]), - ('/paramerrors/one_positional_args/foo', 'param1=foo¶m2=foo', error_msgs[2]), - ('/paramerrors/one_positional_args/foo/bar/baz', 'param2=foo', error_msgs[4]), - ('/paramerrors/one_positional_args_kwargs/foo/bar/baz', 'param1=bar¶m3=baz', error_msgs[2]), - ('/paramerrors/one_positional_kwargs/foo', 'param1=foo¶m2=bar¶m3=baz', error_msgs[2]), - ('/paramerrors/no_positional', 'param1=foo', error_msgs[4]), - ('/paramerrors/no_positional_args/boo', 'param1=foo', error_msgs[4]), - ('/paramerrors/callable_object', 'param1=foo', error_msgs[4]), - ): - for show_mismatched_params in (True, False): - cherrypy.config.update({'request.show_mismatched_params': show_mismatched_params}) - self.getPage(uri, method='POST', body=body) - self.assertStatus(400) - if show_mismatched_params: - self.assertInBody(msg) - else: - self.assertInBody("400 Bad") - - - # even if body parameters are wrong, if we get the uri wrong, then - # it's a 404 - for uri, body, msg in ( - ('/paramerrors/one_positional?param2=foo', 'param1=foo', error_msgs[3]), - ('/paramerrors/one_positional/foo/bar', 'param2=foo', error_msgs[1]), - ('/paramerrors/one_positional_args/foo/bar?param2=foo', 'param3=foo', error_msgs[3]), - ('/paramerrors/one_positional_kwargs/foo/bar', 'param2=bar¶m3=baz', error_msgs[1]), - ('/paramerrors/no_positional?param1=foo', 'param2=foo', error_msgs[3]), - ('/paramerrors/no_positional_args/boo?param2=foo', 'param1=foo', error_msgs[3]), - ('/paramerrors/callable_object?param2=bar', 'param1=foo', error_msgs[3]), - ): - for show_mismatched_params in (True, False): - cherrypy.config.update({'request.show_mismatched_params': show_mismatched_params}) - self.getPage(uri, method='POST', body=body) - self.assertStatus(404) - if show_mismatched_params: - self.assertInBody(msg) - else: - self.assertInBody("Not Found") - - # In the case that a handler raises a TypeError we should - # let that type error through. - for uri in ( - '/paramerrors/raise_type_error', - '/paramerrors/raise_type_error_with_default_param?x=0', - '/paramerrors/raise_type_error_with_default_param?x=0&y=0', - ): - self.getPage(uri, method='GET') - self.assertStatus(500) - self.assertTrue('Client Error', self.body) - - def testErrorHandling(self): - self.getPage("/error/missing") - self.assertStatus(404) - self.assertErrorPage(404, "The path '/error/missing' was not found.") - - ignore = helper.webtest.ignored_exceptions - ignore.append(ValueError) - try: - valerr = '\n raise ValueError()\nValueError' - self.getPage("/error/page_method") - self.assertErrorPage(500, pattern=valerr) - - self.getPage("/error/page_yield") - self.assertErrorPage(500, pattern=valerr) - - if (cherrypy.server.protocol_version == "HTTP/1.0" or - getattr(cherrypy.server, "using_apache", False)): - self.getPage("/error/page_streamed") - # Because this error is raised after the response body has - # started, the status should not change to an error status. - self.assertStatus(200) - self.assertBody("word up") - else: - # Under HTTP/1.1, the chunked transfer-coding is used. - # The HTTP client will choke when the output is incomplete. - self.assertRaises((ValueError, IncompleteRead), self.getPage, - "/error/page_streamed") - - # No traceback should be present - self.getPage("/error/cause_err_in_finalize") - msg = "Illegal response status from server ('ZOO' is non-numeric)." - self.assertErrorPage(500, msg, None) - finally: - ignore.pop() - - # Test HTTPError with a reason-phrase in the status arg. - self.getPage('/error/reason_phrase') - self.assertStatus("410 Gone fishin'") - - # Test custom error page for a specific error. - self.getPage("/error/custom") - self.assertStatus(404) - self.assertBody("Hello, world\r\n" + (" " * 499)) - - # Test custom error page for a specific error. - self.getPage("/error/custom?err=401") - self.assertStatus(401) - self.assertBody("Error 401 Unauthorized - Well, I'm very sorry but you haven't paid!") - - # Test default custom error page. - self.getPage("/error/custom_default") - self.assertStatus(500) - self.assertBody("Error 500 Internal Server Error - Well, I'm very sorry but you haven't paid!".ljust(513)) - - # Test error in custom error page (ticket #305). - # Note that the message is escaped for HTML (ticket #310). - self.getPage("/error/noexist") - self.assertStatus(404) - msg = ("No, <b>really</b>, not found!
" - "In addition, the custom error page failed:\n
" - "IOError: [Errno 2] No such file or directory: 'nonexistent.html'") - self.assertInBody(msg) - - if getattr(cherrypy.server, "using_apache", False): - pass - else: - # Test throw_errors (ticket #186). - self.getPage("/error/rethrow") - self.assertInBody("raise ValueError()") - - def testExpect(self): - e = ('Expect', '100-continue') - self.getPage("/headerelements/get_elements?headername=Expect", [e]) - self.assertBody('100-continue') - - self.getPage("/expect/expectation_failed", [e]) - self.assertStatus(417) - - def testHeaderElements(self): - # Accept-* header elements should be sorted, with most preferred first. - h = [('Accept', 'audio/*; q=0.2, audio/basic')] - self.getPage("/headerelements/get_elements?headername=Accept", h) - self.assertStatus(200) - self.assertBody("audio/basic\n" - "audio/*;q=0.2") - - h = [('Accept', 'text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c')] - self.getPage("/headerelements/get_elements?headername=Accept", h) - self.assertStatus(200) - self.assertBody("text/x-c\n" - "text/html\n" - "text/x-dvi;q=0.8\n" - "text/plain;q=0.5") - - # Test that more specific media ranges get priority. - h = [('Accept', 'text/*, text/html, text/html;level=1, */*')] - self.getPage("/headerelements/get_elements?headername=Accept", h) - self.assertStatus(200) - self.assertBody("text/html;level=1\n" - "text/html\n" - "text/*\n" - "*/*") - - # Test Accept-Charset - h = [('Accept-Charset', 'iso-8859-5, unicode-1-1;q=0.8')] - self.getPage("/headerelements/get_elements?headername=Accept-Charset", h) - self.assertStatus("200 OK") - self.assertBody("iso-8859-5\n" - "unicode-1-1;q=0.8") - - # Test Accept-Encoding - h = [('Accept-Encoding', 'gzip;q=1.0, identity; q=0.5, *;q=0')] - self.getPage("/headerelements/get_elements?headername=Accept-Encoding", h) - self.assertStatus("200 OK") - self.assertBody("gzip;q=1.0\n" - "identity;q=0.5\n" - "*;q=0") - - # Test Accept-Language - h = [('Accept-Language', 'da, en-gb;q=0.8, en;q=0.7')] - self.getPage("/headerelements/get_elements?headername=Accept-Language", h) - self.assertStatus("200 OK") - self.assertBody("da\n" - "en-gb;q=0.8\n" - "en;q=0.7") - - # Test malformed header parsing. See http://www.cherrypy.org/ticket/763. - self.getPage("/headerelements/get_elements?headername=Content-Type", - # Note the illegal trailing ";" - headers=[('Content-Type', 'text/html; charset=utf-8;')]) - self.assertStatus(200) - self.assertBody("text/html;charset=utf-8") - - def test_repeated_headers(self): - # Test that two request headers are collapsed into one. - # See http://www.cherrypy.org/ticket/542. - self.getPage("/headers/Accept-Charset", - headers=[("Accept-Charset", "iso-8859-5"), - ("Accept-Charset", "unicode-1-1;q=0.8")]) - self.assertBody("iso-8859-5, unicode-1-1;q=0.8") - - # Tests that each header only appears once, regardless of case. - self.getPage("/headers/doubledheaders") - self.assertBody("double header test") - hnames = [name.title() for name, val in self.headers] - for key in ['Content-Length', 'Content-Type', 'Date', - 'Expires', 'Location', 'Server']: - self.assertEqual(hnames.count(key), 1, self.headers) - - def test_encoded_headers(self): - # First, make sure the innards work like expected. - self.assertEqual(httputil.decode_TEXT(ntou("=?utf-8?q?f=C3=BCr?=")), ntou("f\xfcr")) - - if cherrypy.server.protocol_version == "HTTP/1.1": - # Test RFC-2047-encoded request and response header values - u = ntou('\u212bngstr\xf6m', 'escape') - c = ntou("=E2=84=ABngstr=C3=B6m") - self.getPage("/headers/ifmatch", [('If-Match', ntou('=?utf-8?q?%s?=') % c)]) - # The body should be utf-8 encoded. - self.assertBody(ntob("\xe2\x84\xabngstr\xc3\xb6m")) - # But the Etag header should be RFC-2047 encoded (binary) - self.assertHeader("ETag", ntou('=?utf-8?b?4oSrbmdzdHLDtm0=?=')) - - # Test a *LONG* RFC-2047-encoded request and response header value - self.getPage("/headers/ifmatch", - [('If-Match', ntou('=?utf-8?q?%s?=') % (c * 10))]) - self.assertBody(ntob("\xe2\x84\xabngstr\xc3\xb6m") * 10) - # Note: this is different output for Python3, but it decodes fine. - etag = self.assertHeader("ETag", - '=?utf-8?b?4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' - '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' - '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt' - '4oSrbmdzdHLDtm0=?=') - self.assertEqual(httputil.decode_TEXT(etag), u * 10) - - def test_header_presence(self): - # If we don't pass a Content-Type header, it should not be present - # in cherrypy.request.headers - self.getPage("/headers/Content-Type", - headers=[]) - self.assertStatus(500) - - # If Content-Type is present in the request, it should be present in - # cherrypy.request.headers - self.getPage("/headers/Content-Type", - headers=[("Content-type", "application/json")]) - self.assertBody("application/json") - - def test_basic_HTTPMethods(self): - helper.webtest.methods_with_bodies = ("POST", "PUT", "PROPFIND") - - # Test that all defined HTTP methods work. - for m in defined_http_methods: - self.getPage("/method/", method=m) - - # HEAD requests should not return any body. - if m == "HEAD": - self.assertBody("") - elif m == "TRACE": - # Some HTTP servers (like modpy) have their own TRACE support - self.assertEqual(self.body[:5], ntob("TRACE")) - else: - self.assertBody(m) - - # Request a PUT method with a form-urlencoded body - self.getPage("/method/parameterized", method="PUT", - body="data=on+top+of+other+things") - self.assertBody("on top of other things") - - # Request a PUT method with a file body - b = "one thing on top of another" - h = [("Content-Type", "text/plain"), - ("Content-Length", str(len(b)))] - self.getPage("/method/request_body", headers=h, method="PUT", body=b) - self.assertStatus(200) - self.assertBody(b) - - # Request a PUT method with a file body but no Content-Type. - # See http://www.cherrypy.org/ticket/790. - b = ntob("one thing on top of another") - self.persistent = True - try: - conn = self.HTTP_CONN - conn.putrequest("PUT", "/method/request_body", skip_host=True) - conn.putheader("Host", self.HOST) - conn.putheader('Content-Length', str(len(b))) - conn.endheaders() - conn.send(b) - response = conn.response_class(conn.sock, method="PUT") - response.begin() - self.assertEqual(response.status, 200) - self.body = response.read() - self.assertBody(b) - finally: - self.persistent = False - - # Request a PUT method with no body whatsoever (not an empty one). - # See http://www.cherrypy.org/ticket/650. - # Provide a C-T or webtest will provide one (and a C-L) for us. - h = [("Content-Type", "text/plain")] - self.getPage("/method/reachable", headers=h, method="PUT") - self.assertStatus(411) - - # Request a custom method with a request body - b = ('\n\n' - '' - '') - h = [('Content-Type', 'text/xml'), - ('Content-Length', str(len(b)))] - self.getPage("/method/request_body", headers=h, method="PROPFIND", body=b) - self.assertStatus(200) - self.assertBody(b) - - # Request a disallowed method - self.getPage("/method/", method="LINK") - self.assertStatus(405) - - # Request an unknown method - self.getPage("/method/", method="SEARCH") - self.assertStatus(501) - - # For method dispatchers: make sure that an HTTP method doesn't - # collide with a virtual path atom. If you build HTTP-method - # dispatching into the core, rewrite these handlers to use - # your dispatch idioms. - self.getPage("/divorce/get?ID=13") - self.assertBody('Divorce document 13: empty') - self.assertStatus(200) - self.getPage("/divorce/", method="GET") - self.assertBody('

Choose your document

\n
    \n
') - self.assertStatus(200) - - def test_CONNECT_method(self): - if getattr(cherrypy.server, "using_apache", False): - return self.skip("skipped due to known Apache differences... ") - - self.getPage("/method/", method="CONNECT") - self.assertBody("CONNECT") - - def testEmptyThreadlocals(self): - results = [] - for x in range(20): - self.getPage("/threadlocal/") - results.append(self.body) - self.assertEqual(results, [ntob("None")] * 20) - diff --git a/libs/cherrypy/test/test_routes.py b/libs/cherrypy/test/test_routes.py deleted file mode 100644 index a8062f8..0000000 --- a/libs/cherrypy/test/test_routes.py +++ /dev/null @@ -1,69 +0,0 @@ -import os -curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - -import cherrypy - -from cherrypy.test import helper -import nose - -class RoutesDispatchTest(helper.CPWebCase): - - def setup_server(): - - try: - import routes - except ImportError: - raise nose.SkipTest('Install routes to test RoutesDispatcher code') - - class Dummy: - def index(self): - return "I said good day!" - - class City: - - def __init__(self, name): - self.name = name - self.population = 10000 - - def index(self, **kwargs): - return "Welcome to %s, pop. %s" % (self.name, self.population) - index._cp_config = {'tools.response_headers.on': True, - 'tools.response_headers.headers': [('Content-Language', 'en-GB')]} - - def update(self, **kwargs): - self.population = kwargs['pop'] - return "OK" - - d = cherrypy.dispatch.RoutesDispatcher() - d.connect(action='index', name='hounslow', route='/hounslow', - controller=City('Hounslow')) - d.connect(name='surbiton', route='/surbiton', controller=City('Surbiton'), - action='index', conditions=dict(method=['GET'])) - d.mapper.connect('/surbiton', controller='surbiton', - action='update', conditions=dict(method=['POST'])) - d.connect('main', ':action', controller=Dummy()) - - conf = {'/': {'request.dispatch': d}} - cherrypy.tree.mount(root=None, config=conf) - setup_server = staticmethod(setup_server) - - def test_Routes_Dispatch(self): - self.getPage("/hounslow") - self.assertStatus("200 OK") - self.assertBody("Welcome to Hounslow, pop. 10000") - - self.getPage("/foo") - self.assertStatus("404 Not Found") - - self.getPage("/surbiton") - self.assertStatus("200 OK") - self.assertBody("Welcome to Surbiton, pop. 10000") - - self.getPage("/surbiton", method="POST", body="pop=1327") - self.assertStatus("200 OK") - self.assertBody("OK") - self.getPage("/surbiton") - self.assertStatus("200 OK") - self.assertHeader("Content-Language", "en-GB") - self.assertBody("Welcome to Surbiton, pop. 1327") - diff --git a/libs/cherrypy/test/test_session.py b/libs/cherrypy/test/test_session.py deleted file mode 100644 index 9143a1d..0000000 --- a/libs/cherrypy/test/test_session.py +++ /dev/null @@ -1,464 +0,0 @@ -import os -localDir = os.path.dirname(__file__) -import sys -import threading -import time - -import cherrypy -from cherrypy._cpcompat import copykeys, HTTPConnection, HTTPSConnection -from cherrypy.lib import sessions -from cherrypy.lib.httputil import response_codes - -def http_methods_allowed(methods=['GET', 'HEAD']): - method = cherrypy.request.method.upper() - if method not in methods: - cherrypy.response.headers['Allow'] = ", ".join(methods) - raise cherrypy.HTTPError(405) - -cherrypy.tools.allow = cherrypy.Tool('on_start_resource', http_methods_allowed) - - -def setup_server(): - - class Root: - - _cp_config = {'tools.sessions.on': True, - 'tools.sessions.storage_type' : 'ram', - 'tools.sessions.storage_path' : localDir, - 'tools.sessions.timeout': (1.0 / 60), - 'tools.sessions.clean_freq': (1.0 / 60), - } - - def clear(self): - cherrypy.session.cache.clear() - clear.exposed = True - - def data(self): - cherrypy.session['aha'] = 'foo' - return repr(cherrypy.session._data) - data.exposed = True - - def testGen(self): - counter = cherrypy.session.get('counter', 0) + 1 - cherrypy.session['counter'] = counter - yield str(counter) - testGen.exposed = True - - def testStr(self): - counter = cherrypy.session.get('counter', 0) + 1 - cherrypy.session['counter'] = counter - return str(counter) - testStr.exposed = True - - def setsessiontype(self, newtype): - self.__class__._cp_config.update({'tools.sessions.storage_type': newtype}) - if hasattr(cherrypy, "session"): - del cherrypy.session - cls = getattr(sessions, newtype.title() + 'Session') - if cls.clean_thread: - cls.clean_thread.stop() - cls.clean_thread.unsubscribe() - del cls.clean_thread - setsessiontype.exposed = True - setsessiontype._cp_config = {'tools.sessions.on': False} - - def index(self): - sess = cherrypy.session - c = sess.get('counter', 0) + 1 - time.sleep(0.01) - sess['counter'] = c - return str(c) - index.exposed = True - - def keyin(self, key): - return str(key in cherrypy.session) - keyin.exposed = True - - def delete(self): - cherrypy.session.delete() - sessions.expire() - return "done" - delete.exposed = True - - def delkey(self, key): - del cherrypy.session[key] - return "OK" - delkey.exposed = True - - def blah(self): - return self._cp_config['tools.sessions.storage_type'] - blah.exposed = True - - def iredir(self): - raise cherrypy.InternalRedirect('/blah') - iredir.exposed = True - - def restricted(self): - return cherrypy.request.method - restricted.exposed = True - restricted._cp_config = {'tools.allow.on': True, - 'tools.allow.methods': ['GET']} - - def regen(self): - cherrypy.tools.sessions.regenerate() - return "logged in" - regen.exposed = True - - def length(self): - return str(len(cherrypy.session)) - length.exposed = True - - def session_cookie(self): - # Must load() to start the clean thread. - cherrypy.session.load() - return cherrypy.session.id - session_cookie.exposed = True - session_cookie._cp_config = { - 'tools.sessions.path': '/session_cookie', - 'tools.sessions.name': 'temp', - 'tools.sessions.persistent': False} - - cherrypy.tree.mount(Root()) - - -from cherrypy.test import helper - -class SessionTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def tearDown(self): - # Clean up sessions. - for fname in os.listdir(localDir): - if fname.startswith(sessions.FileSession.SESSION_PREFIX): - os.unlink(os.path.join(localDir, fname)) - - def test_0_Session(self): - self.getPage('/setsessiontype/ram') - self.getPage('/clear') - - # Test that a normal request gets the same id in the cookies. - # Note: this wouldn't work if /data didn't load the session. - self.getPage('/data') - self.assertBody("{'aha': 'foo'}") - c = self.cookies[0] - self.getPage('/data', self.cookies) - self.assertEqual(self.cookies[0], c) - - self.getPage('/testStr') - self.assertBody('1') - cookie_parts = dict([p.strip().split('=') - for p in self.cookies[0][1].split(";")]) - # Assert there is an 'expires' param - self.assertEqual(set(cookie_parts.keys()), - set(['session_id', 'expires', 'Path'])) - self.getPage('/testGen', self.cookies) - self.assertBody('2') - self.getPage('/testStr', self.cookies) - self.assertBody('3') - self.getPage('/data', self.cookies) - self.assertBody("{'aha': 'foo', 'counter': 3}") - self.getPage('/length', self.cookies) - self.assertBody('2') - self.getPage('/delkey?key=counter', self.cookies) - self.assertStatus(200) - - self.getPage('/setsessiontype/file') - self.getPage('/testStr') - self.assertBody('1') - self.getPage('/testGen', self.cookies) - self.assertBody('2') - self.getPage('/testStr', self.cookies) - self.assertBody('3') - self.getPage('/delkey?key=counter', self.cookies) - self.assertStatus(200) - - # Wait for the session.timeout (1 second) - time.sleep(2) - self.getPage('/') - self.assertBody('1') - self.getPage('/length', self.cookies) - self.assertBody('1') - - # Test session __contains__ - self.getPage('/keyin?key=counter', self.cookies) - self.assertBody("True") - cookieset1 = self.cookies - - # Make a new session and test __len__ again - self.getPage('/') - self.getPage('/length', self.cookies) - self.assertBody('2') - - # Test session delete - self.getPage('/delete', self.cookies) - self.assertBody("done") - self.getPage('/delete', cookieset1) - self.assertBody("done") - f = lambda: [x for x in os.listdir(localDir) if x.startswith('session-')] - self.assertEqual(f(), []) - - # Wait for the cleanup thread to delete remaining session files - self.getPage('/') - f = lambda: [x for x in os.listdir(localDir) if x.startswith('session-')] - self.assertNotEqual(f(), []) - time.sleep(2) - self.assertEqual(f(), []) - - def test_1_Ram_Concurrency(self): - self.getPage('/setsessiontype/ram') - self._test_Concurrency() - - def test_2_File_Concurrency(self): - self.getPage('/setsessiontype/file') - self._test_Concurrency() - - def _test_Concurrency(self): - client_thread_count = 5 - request_count = 30 - - # Get initial cookie - self.getPage("/") - self.assertBody("1") - cookies = self.cookies - - data_dict = {} - errors = [] - - def request(index): - if self.scheme == 'https': - c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT)) - else: - c = HTTPConnection('%s:%s' % (self.interface(), self.PORT)) - for i in range(request_count): - c.putrequest('GET', '/') - for k, v in cookies: - c.putheader(k, v) - c.endheaders() - response = c.getresponse() - body = response.read() - if response.status != 200 or not body.isdigit(): - errors.append((response.status, body)) - else: - data_dict[index] = max(data_dict[index], int(body)) - # Uncomment the following line to prove threads overlap. -## sys.stdout.write("%d " % index) - - # Start requests from each of - # concurrent clients - ts = [] - for c in range(client_thread_count): - data_dict[c] = 0 - t = threading.Thread(target=request, args=(c,)) - ts.append(t) - t.start() - - for t in ts: - t.join() - - hitcount = max(data_dict.values()) - expected = 1 + (client_thread_count * request_count) - - for e in errors: - print(e) - self.assertEqual(hitcount, expected) - - def test_3_Redirect(self): - # Start a new session - self.getPage('/testStr') - self.getPage('/iredir', self.cookies) - self.assertBody("file") - - def test_4_File_deletion(self): - # Start a new session - self.getPage('/testStr') - # Delete the session file manually and retry. - id = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1] - path = os.path.join(localDir, "session-" + id) - os.unlink(path) - self.getPage('/testStr', self.cookies) - - def test_5_Error_paths(self): - self.getPage('/unknown/page') - self.assertErrorPage(404, "The path '/unknown/page' was not found.") - - # Note: this path is *not* the same as above. The above - # takes a normal route through the session code; this one - # skips the session code's before_handler and only calls - # before_finalize (save) and on_end (close). So the session - # code has to survive calling save/close without init. - self.getPage('/restricted', self.cookies, method='POST') - self.assertErrorPage(405, response_codes[405][1]) - - def test_6_regenerate(self): - self.getPage('/testStr') - # grab the cookie ID - id1 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1] - self.getPage('/regen') - self.assertBody('logged in') - id2 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1] - self.assertNotEqual(id1, id2) - - self.getPage('/testStr') - # grab the cookie ID - id1 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1] - self.getPage('/testStr', - headers=[('Cookie', - 'session_id=maliciousid; ' - 'expires=Sat, 27 Oct 2017 04:18:28 GMT; Path=/;')]) - id2 = self.cookies[0][1].split(";", 1)[0].split("=", 1)[1] - self.assertNotEqual(id1, id2) - self.assertNotEqual(id2, 'maliciousid') - - def test_7_session_cookies(self): - self.getPage('/setsessiontype/ram') - self.getPage('/clear') - self.getPage('/session_cookie') - # grab the cookie ID - cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(";")]) - # Assert there is no 'expires' param - self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path'])) - id1 = cookie_parts['temp'] - self.assertEqual(copykeys(sessions.RamSession.cache), [id1]) - - # Send another request in the same "browser session". - self.getPage('/session_cookie', self.cookies) - cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(";")]) - # Assert there is no 'expires' param - self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path'])) - self.assertBody(id1) - self.assertEqual(copykeys(sessions.RamSession.cache), [id1]) - - # Simulate a browser close by just not sending the cookies - self.getPage('/session_cookie') - # grab the cookie ID - cookie_parts = dict([p.strip().split('=') for p in self.cookies[0][1].split(";")]) - # Assert there is no 'expires' param - self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path'])) - # Assert a new id has been generated... - id2 = cookie_parts['temp'] - self.assertNotEqual(id1, id2) - self.assertEqual(set(sessions.RamSession.cache.keys()), set([id1, id2])) - - # Wait for the session.timeout on both sessions - time.sleep(2.5) - cache = copykeys(sessions.RamSession.cache) - if cache: - if cache == [id2]: - self.fail("The second session did not time out.") - else: - self.fail("Unknown session id in cache: %r", cache) - - -import socket -try: - import memcache - - host, port = '127.0.0.1', 11211 - for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, - socket.SOCK_STREAM): - af, socktype, proto, canonname, sa = res - s = None - try: - s = socket.socket(af, socktype, proto) - # See http://groups.google.com/group/cherrypy-users/ - # browse_frm/thread/bbfe5eb39c904fe0 - s.settimeout(1.0) - s.connect((host, port)) - s.close() - except socket.error: - if s: - s.close() - raise - break -except (ImportError, socket.error): - class MemcachedSessionTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test(self): - return self.skip("memcached not reachable ") -else: - class MemcachedSessionTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_0_Session(self): - self.getPage('/setsessiontype/memcached') - - self.getPage('/testStr') - self.assertBody('1') - self.getPage('/testGen', self.cookies) - self.assertBody('2') - self.getPage('/testStr', self.cookies) - self.assertBody('3') - self.getPage('/length', self.cookies) - self.assertErrorPage(500) - self.assertInBody("NotImplementedError") - self.getPage('/delkey?key=counter', self.cookies) - self.assertStatus(200) - - # Wait for the session.timeout (1 second) - time.sleep(1.25) - self.getPage('/') - self.assertBody('1') - - # Test session __contains__ - self.getPage('/keyin?key=counter', self.cookies) - self.assertBody("True") - - # Test session delete - self.getPage('/delete', self.cookies) - self.assertBody("done") - - def test_1_Concurrency(self): - client_thread_count = 5 - request_count = 30 - - # Get initial cookie - self.getPage("/") - self.assertBody("1") - cookies = self.cookies - - data_dict = {} - - def request(index): - for i in range(request_count): - self.getPage("/", cookies) - # Uncomment the following line to prove threads overlap. -## sys.stdout.write("%d " % index) - if not self.body.isdigit(): - self.fail(self.body) - data_dict[index] = v = int(self.body) - - # Start concurrent requests from - # each of clients - ts = [] - for c in range(client_thread_count): - data_dict[c] = 0 - t = threading.Thread(target=request, args=(c,)) - ts.append(t) - t.start() - - for t in ts: - t.join() - - hitcount = max(data_dict.values()) - expected = 1 + (client_thread_count * request_count) - self.assertEqual(hitcount, expected) - - def test_3_Redirect(self): - # Start a new session - self.getPage('/testStr') - self.getPage('/iredir', self.cookies) - self.assertBody("memcached") - - def test_5_Error_paths(self): - self.getPage('/unknown/page') - self.assertErrorPage(404, "The path '/unknown/page' was not found.") - - # Note: this path is *not* the same as above. The above - # takes a normal route through the session code; this one - # skips the session code's before_handler and only calls - # before_finalize (save) and on_end (close). So the session - # code has to survive calling save/close without init. - self.getPage('/restricted', self.cookies, method='POST') - self.assertErrorPage(405, response_codes[405][1]) - diff --git a/libs/cherrypy/test/test_sessionauthenticate.py b/libs/cherrypy/test/test_sessionauthenticate.py deleted file mode 100644 index ab1fe51..0000000 --- a/libs/cherrypy/test/test_sessionauthenticate.py +++ /dev/null @@ -1,62 +0,0 @@ -import cherrypy -from cherrypy.test import helper - - -class SessionAuthenticateTest(helper.CPWebCase): - - def setup_server(): - - def check(username, password): - # Dummy check_username_and_password function - if username != 'test' or password != 'password': - return 'Wrong login/password' - - def augment_params(): - # A simple tool to add some things to request.params - # This is to check to make sure that session_auth can handle request - # params (ticket #780) - cherrypy.request.params["test"] = "test" - - cherrypy.tools.augment_params = cherrypy.Tool('before_handler', - augment_params, None, priority=30) - - class Test: - - _cp_config = {'tools.sessions.on': True, - 'tools.session_auth.on': True, - 'tools.session_auth.check_username_and_password': check, - 'tools.augment_params.on': True, - } - - def index(self, **kwargs): - return "Hi %s, you are logged in" % cherrypy.request.login - index.exposed = True - - cherrypy.tree.mount(Test()) - setup_server = staticmethod(setup_server) - - - def testSessionAuthenticate(self): - # request a page and check for login form - self.getPage('/') - self.assertInBody('
') - - # setup credentials - login_body = 'username=test&password=password&from_page=/' - - # attempt a login - self.getPage('/do_login', method='POST', body=login_body) - self.assertStatus((302, 303)) - - # get the page now that we are logged in - self.getPage('/', self.cookies) - self.assertBody('Hi test, you are logged in') - - # do a logout - self.getPage('/do_logout', self.cookies, method='POST') - self.assertStatus((302, 303)) - - # verify we are logged out - self.getPage('/', self.cookies) - self.assertInBody('') - diff --git a/libs/cherrypy/test/test_states.py b/libs/cherrypy/test/test_states.py deleted file mode 100644 index 6322687..0000000 --- a/libs/cherrypy/test/test_states.py +++ /dev/null @@ -1,439 +0,0 @@ -from cherrypy._cpcompat import BadStatusLine, ntob -import os -import sys -import threading -import time - -import cherrypy -engine = cherrypy.engine -thisdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - - -class Dependency: - - def __init__(self, bus): - self.bus = bus - self.running = False - self.startcount = 0 - self.gracecount = 0 - self.threads = {} - - def subscribe(self): - self.bus.subscribe('start', self.start) - self.bus.subscribe('stop', self.stop) - self.bus.subscribe('graceful', self.graceful) - self.bus.subscribe('start_thread', self.startthread) - self.bus.subscribe('stop_thread', self.stopthread) - - def start(self): - self.running = True - self.startcount += 1 - - def stop(self): - self.running = False - - def graceful(self): - self.gracecount += 1 - - def startthread(self, thread_id): - self.threads[thread_id] = None - - def stopthread(self, thread_id): - del self.threads[thread_id] - -db_connection = Dependency(engine) - -def setup_server(): - class Root: - def index(self): - return "Hello World" - index.exposed = True - - def ctrlc(self): - raise KeyboardInterrupt() - ctrlc.exposed = True - - def graceful(self): - engine.graceful() - return "app was (gracefully) restarted succesfully" - graceful.exposed = True - - def block_explicit(self): - while True: - if cherrypy.response.timed_out: - cherrypy.response.timed_out = False - return "broken!" - time.sleep(0.01) - block_explicit.exposed = True - - def block_implicit(self): - time.sleep(0.5) - return "response.timeout = %s" % cherrypy.response.timeout - block_implicit.exposed = True - - cherrypy.tree.mount(Root()) - cherrypy.config.update({ - 'environment': 'test_suite', - 'engine.deadlock_poll_freq': 0.1, - }) - - db_connection.subscribe() - - - -# ------------ Enough helpers. Time for real live test cases. ------------ # - - -from cherrypy.test import helper - -class ServerStateTests(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def setUp(self): - cherrypy.server.socket_timeout = 0.1 - self.do_gc_test = False - - def test_0_NormalStateFlow(self): - engine.stop() - # Our db_connection should not be running - self.assertEqual(db_connection.running, False) - self.assertEqual(db_connection.startcount, 1) - self.assertEqual(len(db_connection.threads), 0) - - # Test server start - engine.start() - self.assertEqual(engine.state, engine.states.STARTED) - - host = cherrypy.server.socket_host - port = cherrypy.server.socket_port - self.assertRaises(IOError, cherrypy._cpserver.check_port, host, port) - - # The db_connection should be running now - self.assertEqual(db_connection.running, True) - self.assertEqual(db_connection.startcount, 2) - self.assertEqual(len(db_connection.threads), 0) - - self.getPage("/") - self.assertBody("Hello World") - self.assertEqual(len(db_connection.threads), 1) - - # Test engine stop. This will also stop the HTTP server. - engine.stop() - self.assertEqual(engine.state, engine.states.STOPPED) - - # Verify that our custom stop function was called - self.assertEqual(db_connection.running, False) - self.assertEqual(len(db_connection.threads), 0) - - # Block the main thread now and verify that exit() works. - def exittest(): - self.getPage("/") - self.assertBody("Hello World") - engine.exit() - cherrypy.server.start() - engine.start_with_callback(exittest) - engine.block() - self.assertEqual(engine.state, engine.states.EXITING) - - def test_1_Restart(self): - cherrypy.server.start() - engine.start() - - # The db_connection should be running now - self.assertEqual(db_connection.running, True) - grace = db_connection.gracecount - - self.getPage("/") - self.assertBody("Hello World") - self.assertEqual(len(db_connection.threads), 1) - - # Test server restart from this thread - engine.graceful() - self.assertEqual(engine.state, engine.states.STARTED) - self.getPage("/") - self.assertBody("Hello World") - self.assertEqual(db_connection.running, True) - self.assertEqual(db_connection.gracecount, grace + 1) - self.assertEqual(len(db_connection.threads), 1) - - # Test server restart from inside a page handler - self.getPage("/graceful") - self.assertEqual(engine.state, engine.states.STARTED) - self.assertBody("app was (gracefully) restarted succesfully") - self.assertEqual(db_connection.running, True) - self.assertEqual(db_connection.gracecount, grace + 2) - # Since we are requesting synchronously, is only one thread used? - # Note that the "/graceful" request has been flushed. - self.assertEqual(len(db_connection.threads), 0) - - engine.stop() - self.assertEqual(engine.state, engine.states.STOPPED) - self.assertEqual(db_connection.running, False) - self.assertEqual(len(db_connection.threads), 0) - - def test_2_KeyboardInterrupt(self): - # Raise a keyboard interrupt in the HTTP server's main thread. - # We must start the server in this, the main thread - engine.start() - cherrypy.server.start() - - self.persistent = True - try: - # Make the first request and assert there's no "Connection: close". - self.getPage("/") - self.assertStatus('200 OK') - self.assertBody("Hello World") - self.assertNoHeader("Connection") - - cherrypy.server.httpserver.interrupt = KeyboardInterrupt - engine.block() - - self.assertEqual(db_connection.running, False) - self.assertEqual(len(db_connection.threads), 0) - self.assertEqual(engine.state, engine.states.EXITING) - finally: - self.persistent = False - - # Raise a keyboard interrupt in a page handler; on multithreaded - # servers, this should occur in one of the worker threads. - # This should raise a BadStatusLine error, since the worker - # thread will just die without writing a response. - engine.start() - cherrypy.server.start() - - try: - self.getPage("/ctrlc") - except BadStatusLine: - pass - else: - print(self.body) - self.fail("AssertionError: BadStatusLine not raised") - - engine.block() - self.assertEqual(db_connection.running, False) - self.assertEqual(len(db_connection.threads), 0) - - def test_3_Deadlocks(self): - cherrypy.config.update({'response.timeout': 0.2}) - - engine.start() - cherrypy.server.start() - try: - self.assertNotEqual(engine.timeout_monitor.thread, None) - - # Request a "normal" page. - self.assertEqual(engine.timeout_monitor.servings, []) - self.getPage("/") - self.assertBody("Hello World") - # request.close is called async. - while engine.timeout_monitor.servings: - sys.stdout.write(".") - time.sleep(0.01) - - # Request a page that explicitly checks itself for deadlock. - # The deadlock_timeout should be 2 secs. - self.getPage("/block_explicit") - self.assertBody("broken!") - - # Request a page that implicitly breaks deadlock. - # If we deadlock, we want to touch as little code as possible, - # so we won't even call handle_error, just bail ASAP. - self.getPage("/block_implicit") - self.assertStatus(500) - self.assertInBody("raise cherrypy.TimeoutError()") - finally: - engine.exit() - - def test_4_Autoreload(self): - # Start the demo script in a new process - p = helper.CPProcess(ssl=(self.scheme.lower()=='https')) - p.write_conf( - extra='test_case_name: "test_4_Autoreload"') - p.start(imports='cherrypy.test._test_states_demo') - try: - self.getPage("/start") - start = float(self.body) - - # Give the autoreloader time to cache the file time. - time.sleep(2) - - # Touch the file - os.utime(os.path.join(thisdir, "_test_states_demo.py"), None) - - # Give the autoreloader time to re-exec the process - time.sleep(2) - host = cherrypy.server.socket_host - port = cherrypy.server.socket_port - cherrypy._cpserver.wait_for_occupied_port(host, port) - - self.getPage("/start") - if not (float(self.body) > start): - raise AssertionError("start time %s not greater than %s" % - (float(self.body), start)) - finally: - # Shut down the spawned process - self.getPage("/exit") - p.join() - - def test_5_Start_Error(self): - # If a process errors during start, it should stop the engine - # and exit with a non-zero exit code. - p = helper.CPProcess(ssl=(self.scheme.lower()=='https'), - wait=True) - p.write_conf( - extra="""starterror: True -test_case_name: "test_5_Start_Error" -""" - ) - p.start(imports='cherrypy.test._test_states_demo') - if p.exit_code == 0: - self.fail("Process failed to return nonzero exit code.") - - -class PluginTests(helper.CPWebCase): - def test_daemonize(self): - if os.name not in ['posix']: - return self.skip("skipped (not on posix) ") - self.HOST = '127.0.0.1' - self.PORT = 8081 - # Spawn the process and wait, when this returns, the original process - # is finished. If it daemonized properly, we should still be able - # to access pages. - p = helper.CPProcess(ssl=(self.scheme.lower()=='https'), - wait=True, daemonize=True, - socket_host='127.0.0.1', - socket_port=8081) - p.write_conf( - extra='test_case_name: "test_daemonize"') - p.start(imports='cherrypy.test._test_states_demo') - try: - # Just get the pid of the daemonization process. - self.getPage("/pid") - self.assertStatus(200) - page_pid = int(self.body) - self.assertEqual(page_pid, p.get_pid()) - finally: - # Shut down the spawned process - self.getPage("/exit") - p.join() - - # Wait until here to test the exit code because we want to ensure - # that we wait for the daemon to finish running before we fail. - if p.exit_code != 0: - self.fail("Daemonized parent process failed to exit cleanly.") - - -class SignalHandlingTests(helper.CPWebCase): - def test_SIGHUP_tty(self): - # When not daemonized, SIGHUP should shut down the server. - try: - from signal import SIGHUP - except ImportError: - return self.skip("skipped (no SIGHUP) ") - - # Spawn the process. - p = helper.CPProcess(ssl=(self.scheme.lower()=='https')) - p.write_conf( - extra='test_case_name: "test_SIGHUP_tty"') - p.start(imports='cherrypy.test._test_states_demo') - # Send a SIGHUP - os.kill(p.get_pid(), SIGHUP) - # This might hang if things aren't working right, but meh. - p.join() - - def test_SIGHUP_daemonized(self): - # When daemonized, SIGHUP should restart the server. - try: - from signal import SIGHUP - except ImportError: - return self.skip("skipped (no SIGHUP) ") - - if os.name not in ['posix']: - return self.skip("skipped (not on posix) ") - - # Spawn the process and wait, when this returns, the original process - # is finished. If it daemonized properly, we should still be able - # to access pages. - p = helper.CPProcess(ssl=(self.scheme.lower()=='https'), - wait=True, daemonize=True) - p.write_conf( - extra='test_case_name: "test_SIGHUP_daemonized"') - p.start(imports='cherrypy.test._test_states_demo') - - pid = p.get_pid() - try: - # Send a SIGHUP - os.kill(pid, SIGHUP) - # Give the server some time to restart - time.sleep(2) - self.getPage("/pid") - self.assertStatus(200) - new_pid = int(self.body) - self.assertNotEqual(new_pid, pid) - finally: - # Shut down the spawned process - self.getPage("/exit") - p.join() - - def test_SIGTERM(self): - # SIGTERM should shut down the server whether daemonized or not. - try: - from signal import SIGTERM - except ImportError: - return self.skip("skipped (no SIGTERM) ") - - try: - from os import kill - except ImportError: - return self.skip("skipped (no os.kill) ") - - # Spawn a normal, undaemonized process. - p = helper.CPProcess(ssl=(self.scheme.lower()=='https')) - p.write_conf( - extra='test_case_name: "test_SIGTERM"') - p.start(imports='cherrypy.test._test_states_demo') - # Send a SIGTERM - os.kill(p.get_pid(), SIGTERM) - # This might hang if things aren't working right, but meh. - p.join() - - if os.name in ['posix']: - # Spawn a daemonized process and test again. - p = helper.CPProcess(ssl=(self.scheme.lower()=='https'), - wait=True, daemonize=True) - p.write_conf( - extra='test_case_name: "test_SIGTERM_2"') - p.start(imports='cherrypy.test._test_states_demo') - # Send a SIGTERM - os.kill(p.get_pid(), SIGTERM) - # This might hang if things aren't working right, but meh. - p.join() - - def test_signal_handler_unsubscribe(self): - try: - from signal import SIGTERM - except ImportError: - return self.skip("skipped (no SIGTERM) ") - - try: - from os import kill - except ImportError: - return self.skip("skipped (no os.kill) ") - - # Spawn a normal, undaemonized process. - p = helper.CPProcess(ssl=(self.scheme.lower()=='https')) - p.write_conf( - extra="""unsubsig: True -test_case_name: "test_signal_handler_unsubscribe" -""") - p.start(imports='cherrypy.test._test_states_demo') - # Send a SIGTERM - os.kill(p.get_pid(), SIGTERM) - # This might hang if things aren't working right, but meh. - p.join() - - # Assert the old handler ran. - target_line = open(p.error_log, 'rb').readlines()[-10] - if not ntob("I am an old SIGTERM handler.") in target_line: - self.fail("Old SIGTERM handler did not run.\n%r" % target_line) - diff --git a/libs/cherrypy/test/test_static.py b/libs/cherrypy/test/test_static.py deleted file mode 100644 index 871420b..0000000 --- a/libs/cherrypy/test/test_static.py +++ /dev/null @@ -1,300 +0,0 @@ -from cherrypy._cpcompat import HTTPConnection, HTTPSConnection, ntob -from cherrypy._cpcompat import BytesIO - -import os -curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) -has_space_filepath = os.path.join(curdir, 'static', 'has space.html') -bigfile_filepath = os.path.join(curdir, "static", "bigfile.log") -BIGFILE_SIZE = 1024 * 1024 -import threading - -import cherrypy -from cherrypy.lib import static -from cherrypy.test import helper - - -class StaticTest(helper.CPWebCase): - - def setup_server(): - if not os.path.exists(has_space_filepath): - open(has_space_filepath, 'wb').write(ntob('Hello, world\r\n')) - if not os.path.exists(bigfile_filepath): - open(bigfile_filepath, 'wb').write(ntob("x" * BIGFILE_SIZE)) - - class Root: - - def bigfile(self): - from cherrypy.lib import static - self.f = static.serve_file(bigfile_filepath) - return self.f - bigfile.exposed = True - bigfile._cp_config = {'response.stream': True} - - def tell(self): - if self.f.input.closed: - return '' - return repr(self.f.input.tell()).rstrip('L') - tell.exposed = True - - def fileobj(self): - f = open(os.path.join(curdir, 'style.css'), 'rb') - return static.serve_fileobj(f, content_type='text/css') - fileobj.exposed = True - - def bytesio(self): - f = BytesIO(ntob('Fee\nfie\nfo\nfum')) - return static.serve_fileobj(f, content_type='text/plain') - bytesio.exposed = True - - class Static: - - def index(self): - return 'You want the Baron? You can have the Baron!' - index.exposed = True - - def dynamic(self): - return "This is a DYNAMIC page" - dynamic.exposed = True - - - root = Root() - root.static = Static() - - rootconf = { - '/static': { - 'tools.staticdir.on': True, - 'tools.staticdir.dir': 'static', - 'tools.staticdir.root': curdir, - }, - '/style.css': { - 'tools.staticfile.on': True, - 'tools.staticfile.filename': os.path.join(curdir, 'style.css'), - }, - '/docroot': { - 'tools.staticdir.on': True, - 'tools.staticdir.root': curdir, - 'tools.staticdir.dir': 'static', - 'tools.staticdir.index': 'index.html', - }, - '/error': { - 'tools.staticdir.on': True, - 'request.show_tracebacks': True, - }, - } - rootApp = cherrypy.Application(root) - rootApp.merge(rootconf) - - test_app_conf = { - '/test': { - 'tools.staticdir.index': 'index.html', - 'tools.staticdir.on': True, - 'tools.staticdir.root': curdir, - 'tools.staticdir.dir': 'static', - }, - } - testApp = cherrypy.Application(Static()) - testApp.merge(test_app_conf) - - vhost = cherrypy._cpwsgi.VirtualHost(rootApp, {'virt.net': testApp}) - cherrypy.tree.graft(vhost) - setup_server = staticmethod(setup_server) - - - def teardown_server(): - for f in (has_space_filepath, bigfile_filepath): - if os.path.exists(f): - try: - os.unlink(f) - except: - pass - teardown_server = staticmethod(teardown_server) - - - def testStatic(self): - self.getPage("/static/index.html") - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/html') - self.assertBody('Hello, world\r\n') - - # Using a staticdir.root value in a subdir... - self.getPage("/docroot/index.html") - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/html') - self.assertBody('Hello, world\r\n') - - # Check a filename with spaces in it - self.getPage("/static/has%20space.html") - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/html') - self.assertBody('Hello, world\r\n') - - self.getPage("/style.css") - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/css') - # Note: The body should be exactly 'Dummy stylesheet\n', but - # unfortunately some tools such as WinZip sometimes turn \n - # into \r\n on Windows when extracting the CherryPy tarball so - # we just check the content - self.assertMatchesBody('^Dummy stylesheet') - - def test_fallthrough(self): - # Test that NotFound will then try dynamic handlers (see [878]). - self.getPage("/static/dynamic") - self.assertBody("This is a DYNAMIC page") - - # Check a directory via fall-through to dynamic handler. - self.getPage("/static/") - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/html;charset=utf-8') - self.assertBody('You want the Baron? You can have the Baron!') - - def test_index(self): - # Check a directory via "staticdir.index". - self.getPage("/docroot/") - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/html') - self.assertBody('Hello, world\r\n') - # The same page should be returned even if redirected. - self.getPage("/docroot") - self.assertStatus(301) - self.assertHeader('Location', '%s/docroot/' % self.base()) - self.assertMatchesBody("This resource .* " - "%s/docroot/." % (self.base(), self.base())) - - def test_config_errors(self): - # Check that we get an error if no .file or .dir - self.getPage("/error/thing.html") - self.assertErrorPage(500) - self.assertMatchesBody(ntob("TypeError: staticdir\(\) takes at least 2 " - "(positional )?arguments \(0 given\)")) - - def test_security(self): - # Test up-level security - self.getPage("/static/../../test/style.css") - self.assertStatus((400, 403)) - - def test_modif(self): - # Test modified-since on a reasonably-large file - self.getPage("/static/dirback.jpg") - self.assertStatus("200 OK") - lastmod = "" - for k, v in self.headers: - if k == 'Last-Modified': - lastmod = v - ims = ("If-Modified-Since", lastmod) - self.getPage("/static/dirback.jpg", headers=[ims]) - self.assertStatus(304) - self.assertNoHeader("Content-Type") - self.assertNoHeader("Content-Length") - self.assertNoHeader("Content-Disposition") - self.assertBody("") - - def test_755_vhost(self): - self.getPage("/test/", [('Host', 'virt.net')]) - self.assertStatus(200) - self.getPage("/test", [('Host', 'virt.net')]) - self.assertStatus(301) - self.assertHeader('Location', self.scheme + '://virt.net/test/') - - def test_serve_fileobj(self): - self.getPage("/fileobj") - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/css;charset=utf-8') - self.assertMatchesBody('^Dummy stylesheet') - - def test_serve_bytesio(self): - self.getPage("/bytesio") - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/plain;charset=utf-8') - self.assertHeader('Content-Length', 14) - self.assertMatchesBody('Fee\nfie\nfo\nfum') - - def test_file_stream(self): - if cherrypy.server.protocol_version != "HTTP/1.1": - return self.skip() - - self.PROTOCOL = "HTTP/1.1" - - # Make an initial request - self.persistent = True - conn = self.HTTP_CONN - conn.putrequest("GET", "/bigfile", skip_host=True) - conn.putheader("Host", self.HOST) - conn.endheaders() - response = conn.response_class(conn.sock, method="GET") - response.begin() - self.assertEqual(response.status, 200) - - body = ntob('') - remaining = BIGFILE_SIZE - while remaining > 0: - data = response.fp.read(65536) - if not data: - break - body += data - remaining -= len(data) - - if self.scheme == "https": - newconn = HTTPSConnection - else: - newconn = HTTPConnection - s, h, b = helper.webtest.openURL( - ntob("/tell"), headers=[], host=self.HOST, port=self.PORT, - http_conn=newconn) - if not b: - # The file was closed on the server. - tell_position = BIGFILE_SIZE - else: - tell_position = int(b) - - expected = len(body) - if tell_position >= BIGFILE_SIZE: - # We can't exactly control how much content the server asks for. - # Fudge it by only checking the first half of the reads. - if expected < (BIGFILE_SIZE / 2): - self.fail( - "The file should have advanced to position %r, but has " - "already advanced to the end of the file. It may not be " - "streamed as intended, or at the wrong chunk size (64k)" % - expected) - elif tell_position < expected: - self.fail( - "The file should have advanced to position %r, but has " - "only advanced to position %r. It may not be streamed " - "as intended, or at the wrong chunk size (65536)" % - (expected, tell_position)) - - if body != ntob("x" * BIGFILE_SIZE): - self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." % - (BIGFILE_SIZE, body[:50], len(body))) - conn.close() - - def test_file_stream_deadlock(self): - if cherrypy.server.protocol_version != "HTTP/1.1": - return self.skip() - - self.PROTOCOL = "HTTP/1.1" - - # Make an initial request but abort early. - self.persistent = True - conn = self.HTTP_CONN - conn.putrequest("GET", "/bigfile", skip_host=True) - conn.putheader("Host", self.HOST) - conn.endheaders() - response = conn.response_class(conn.sock, method="GET") - response.begin() - self.assertEqual(response.status, 200) - body = response.fp.read(65536) - if body != ntob("x" * len(body)): - self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." % - (65536, body[:50], len(body))) - response.close() - conn.close() - - # Make a second request, which should fetch the whole file. - self.persistent = False - self.getPage("/bigfile") - if self.body != ntob("x" * BIGFILE_SIZE): - self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." % - (BIGFILE_SIZE, self.body[:50], len(body))) - diff --git a/libs/cherrypy/test/test_tools.py b/libs/cherrypy/test/test_tools.py deleted file mode 100644 index 02bacda..0000000 --- a/libs/cherrypy/test/test_tools.py +++ /dev/null @@ -1,399 +0,0 @@ -"""Test the various means of instantiating and invoking tools.""" - -import gzip -import sys -from cherrypy._cpcompat import BytesIO, copyitems, itervalues -from cherrypy._cpcompat import IncompleteRead, ntob, ntou, py3k, xrange -import time -timeout = 0.2 -import types - -import cherrypy -from cherrypy import tools - - -europoundUnicode = ntou('\x80\xa3') - - -# Client-side code # - -from cherrypy.test import helper - - -class ToolTests(helper.CPWebCase): - def setup_server(): - - # Put check_access in a custom toolbox with its own namespace - myauthtools = cherrypy._cptools.Toolbox("myauth") - - def check_access(default=False): - if not getattr(cherrypy.request, "userid", default): - raise cherrypy.HTTPError(401) - myauthtools.check_access = cherrypy.Tool('before_request_body', check_access) - - def numerify(): - def number_it(body): - for chunk in body: - for k, v in cherrypy.request.numerify_map: - chunk = chunk.replace(k, v) - yield chunk - cherrypy.response.body = number_it(cherrypy.response.body) - - class NumTool(cherrypy.Tool): - def _setup(self): - def makemap(): - m = self._merged_args().get("map", {}) - cherrypy.request.numerify_map = copyitems(m) - cherrypy.request.hooks.attach('on_start_resource', makemap) - - def critical(): - cherrypy.request.error_response = cherrypy.HTTPError(502).set_response - critical.failsafe = True - - cherrypy.request.hooks.attach('on_start_resource', critical) - cherrypy.request.hooks.attach(self._point, self.callable) - - tools.numerify = NumTool('before_finalize', numerify) - - # It's not mandatory to inherit from cherrypy.Tool. - class NadsatTool: - - def __init__(self): - self.ended = {} - self._name = "nadsat" - - def nadsat(self): - def nadsat_it_up(body): - for chunk in body: - chunk = chunk.replace(ntob("good"), ntob("horrorshow")) - chunk = chunk.replace(ntob("piece"), ntob("lomtick")) - yield chunk - cherrypy.response.body = nadsat_it_up(cherrypy.response.body) - nadsat.priority = 0 - - def cleanup(self): - # This runs after the request has been completely written out. - cherrypy.response.body = [ntob("razdrez")] - id = cherrypy.request.params.get("id") - if id: - self.ended[id] = True - cleanup.failsafe = True - - def _setup(self): - cherrypy.request.hooks.attach('before_finalize', self.nadsat) - cherrypy.request.hooks.attach('on_end_request', self.cleanup) - tools.nadsat = NadsatTool() - - def pipe_body(): - cherrypy.request.process_request_body = False - clen = int(cherrypy.request.headers['Content-Length']) - cherrypy.request.body = cherrypy.request.rfile.read(clen) - - # Assert that we can use a callable object instead of a function. - class Rotator(object): - def __call__(self, scale): - r = cherrypy.response - r.collapse_body() - if py3k: - r.body = [bytes([(x + scale) % 256 for x in r.body[0]])] - else: - r.body = [chr((ord(x) + scale) % 256) for x in r.body[0]] - cherrypy.tools.rotator = cherrypy.Tool('before_finalize', Rotator()) - - def stream_handler(next_handler, *args, **kwargs): - cherrypy.response.output = o = BytesIO() - try: - response = next_handler(*args, **kwargs) - # Ignore the response and return our accumulated output instead. - return o.getvalue() - finally: - o.close() - cherrypy.tools.streamer = cherrypy._cptools.HandlerWrapperTool(stream_handler) - - class Root: - def index(self): - return "Howdy earth!" - index.exposed = True - - def tarfile(self): - cherrypy.response.output.write(ntob('I am ')) - cherrypy.response.output.write(ntob('a tarfile')) - tarfile.exposed = True - tarfile._cp_config = {'tools.streamer.on': True} - - def euro(self): - hooks = list(cherrypy.request.hooks['before_finalize']) - hooks.sort() - cbnames = [x.callback.__name__ for x in hooks] - assert cbnames == ['gzip'], cbnames - priorities = [x.priority for x in hooks] - assert priorities == [80], priorities - yield ntou("Hello,") - yield ntou("world") - yield europoundUnicode - euro.exposed = True - - # Bare hooks - def pipe(self): - return cherrypy.request.body - pipe.exposed = True - pipe._cp_config = {'hooks.before_request_body': pipe_body} - - # Multiple decorators; include kwargs just for fun. - # Note that rotator must run before gzip. - def decorated_euro(self, *vpath): - yield ntou("Hello,") - yield ntou("world") - yield europoundUnicode - decorated_euro.exposed = True - decorated_euro = tools.gzip(compress_level=6)(decorated_euro) - decorated_euro = tools.rotator(scale=3)(decorated_euro) - - root = Root() - - - class TestType(type): - """Metaclass which automatically exposes all functions in each subclass, - and adds an instance of the subclass as an attribute of root. - """ - def __init__(cls, name, bases, dct): - type.__init__(cls, name, bases, dct) - for value in itervalues(dct): - if isinstance(value, types.FunctionType): - value.exposed = True - setattr(root, name.lower(), cls()) - Test = TestType('Test', (object,), {}) - - - # METHOD ONE: - # Declare Tools in _cp_config - class Demo(Test): - - _cp_config = {"tools.nadsat.on": True} - - def index(self, id=None): - return "A good piece of cherry pie" - - def ended(self, id): - return repr(tools.nadsat.ended[id]) - - def err(self, id=None): - raise ValueError() - - def errinstream(self, id=None): - yield "nonconfidential" - raise ValueError() - yield "confidential" - - # METHOD TWO: decorator using Tool() - # We support Python 2.3, but the @-deco syntax would look like this: - # @tools.check_access() - def restricted(self): - return "Welcome!" - restricted = myauthtools.check_access()(restricted) - userid = restricted - - def err_in_onstart(self): - return "success!" - - def stream(self, id=None): - for x in xrange(100000000): - yield str(x) - stream._cp_config = {'response.stream': True} - - - conf = { - # METHOD THREE: - # Declare Tools in detached config - '/demo': { - 'tools.numerify.on': True, - 'tools.numerify.map': {ntob("pie"): ntob("3.14159")}, - }, - '/demo/restricted': { - 'request.show_tracebacks': False, - }, - '/demo/userid': { - 'request.show_tracebacks': False, - 'myauth.check_access.default': True, - }, - '/demo/errinstream': { - 'response.stream': True, - }, - '/demo/err_in_onstart': { - # Because this isn't a dict, on_start_resource will error. - 'tools.numerify.map': "pie->3.14159" - }, - # Combined tools - '/euro': { - 'tools.gzip.on': True, - 'tools.encode.on': True, - }, - # Priority specified in config - '/decorated_euro/subpath': { - 'tools.gzip.priority': 10, - }, - # Handler wrappers - '/tarfile': {'tools.streamer.on': True} - } - app = cherrypy.tree.mount(root, config=conf) - app.request_class.namespaces['myauth'] = myauthtools - - if sys.version_info >= (2, 5): - from cherrypy.test import _test_decorators - root.tooldecs = _test_decorators.ToolExamples() - setup_server = staticmethod(setup_server) - - def testHookErrors(self): - self.getPage("/demo/?id=1") - # If body is "razdrez", then on_end_request is being called too early. - self.assertBody("A horrorshow lomtick of cherry 3.14159") - # If this fails, then on_end_request isn't being called at all. - time.sleep(0.1) - self.getPage("/demo/ended/1") - self.assertBody("True") - - valerr = '\n raise ValueError()\nValueError' - self.getPage("/demo/err?id=3") - # If body is "razdrez", then on_end_request is being called too early. - self.assertErrorPage(502, pattern=valerr) - # If this fails, then on_end_request isn't being called at all. - time.sleep(0.1) - self.getPage("/demo/ended/3") - self.assertBody("True") - - # If body is "razdrez", then on_end_request is being called too early. - if (cherrypy.server.protocol_version == "HTTP/1.0" or - getattr(cherrypy.server, "using_apache", False)): - self.getPage("/demo/errinstream?id=5") - # Because this error is raised after the response body has - # started, the status should not change to an error status. - self.assertStatus("200 OK") - self.assertBody("nonconfidential") - else: - # Because this error is raised after the response body has - # started, and because it's chunked output, an error is raised by - # the HTTP client when it encounters incomplete output. - self.assertRaises((ValueError, IncompleteRead), self.getPage, - "/demo/errinstream?id=5") - # If this fails, then on_end_request isn't being called at all. - time.sleep(0.1) - self.getPage("/demo/ended/5") - self.assertBody("True") - - # Test the "__call__" technique (compile-time decorator). - self.getPage("/demo/restricted") - self.assertErrorPage(401) - - # Test compile-time decorator with kwargs from config. - self.getPage("/demo/userid") - self.assertBody("Welcome!") - - def testEndRequestOnDrop(self): - old_timeout = None - try: - httpserver = cherrypy.server.httpserver - old_timeout = httpserver.timeout - except (AttributeError, IndexError): - return self.skip() - - try: - httpserver.timeout = timeout - - # Test that on_end_request is called even if the client drops. - self.persistent = True - try: - conn = self.HTTP_CONN - conn.putrequest("GET", "/demo/stream?id=9", skip_host=True) - conn.putheader("Host", self.HOST) - conn.endheaders() - # Skip the rest of the request and close the conn. This will - # cause the server's active socket to error, which *should* - # result in the request being aborted, and request.close being - # called all the way up the stack (including WSGI middleware), - # eventually calling our on_end_request hook. - finally: - self.persistent = False - time.sleep(timeout * 2) - # Test that the on_end_request hook was called. - self.getPage("/demo/ended/9") - self.assertBody("True") - finally: - if old_timeout is not None: - httpserver.timeout = old_timeout - - def testGuaranteedHooks(self): - # The 'critical' on_start_resource hook is 'failsafe' (guaranteed - # to run even if there are failures in other on_start methods). - # This is NOT true of the other hooks. - # Here, we have set up a failure in NumerifyTool.numerify_map, - # but our 'critical' hook should run and set the error to 502. - self.getPage("/demo/err_in_onstart") - self.assertErrorPage(502) - self.assertInBody("AttributeError: 'str' object has no attribute 'items'") - - def testCombinedTools(self): - expectedResult = (ntou("Hello,world") + europoundUnicode).encode('utf-8') - zbuf = BytesIO() - zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9) - zfile.write(expectedResult) - zfile.close() - - self.getPage("/euro", headers=[("Accept-Encoding", "gzip"), - ("Accept-Charset", "ISO-8859-1,utf-8;q=0.7,*;q=0.7")]) - self.assertInBody(zbuf.getvalue()[:3]) - - zbuf = BytesIO() - zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=6) - zfile.write(expectedResult) - zfile.close() - - self.getPage("/decorated_euro", headers=[("Accept-Encoding", "gzip")]) - self.assertInBody(zbuf.getvalue()[:3]) - - # This returns a different value because gzip's priority was - # lowered in conf, allowing the rotator to run after gzip. - # Of course, we don't want breakage in production apps, - # but it proves the priority was changed. - self.getPage("/decorated_euro/subpath", - headers=[("Accept-Encoding", "gzip")]) - if py3k: - self.assertInBody(bytes([(x + 3) % 256 for x in zbuf.getvalue()])) - else: - self.assertInBody(''.join([chr((ord(x) + 3) % 256) for x in zbuf.getvalue()])) - - def testBareHooks(self): - content = "bit of a pain in me gulliver" - self.getPage("/pipe", - headers=[("Content-Length", str(len(content))), - ("Content-Type", "text/plain")], - method="POST", body=content) - self.assertBody(content) - - def testHandlerWrapperTool(self): - self.getPage("/tarfile") - self.assertBody("I am a tarfile") - - def testToolWithConfig(self): - if not sys.version_info >= (2, 5): - return self.skip("skipped (Python 2.5+ only)") - - self.getPage('/tooldecs/blah') - self.assertHeader('Content-Type', 'application/data') - - def testWarnToolOn(self): - # get - try: - numon = cherrypy.tools.numerify.on - except AttributeError: - pass - else: - raise AssertionError("Tool.on did not error as it should have.") - - # set - try: - cherrypy.tools.numerify.on = True - except AttributeError: - pass - else: - raise AssertionError("Tool.on did not error as it should have.") - diff --git a/libs/cherrypy/test/test_tutorials.py b/libs/cherrypy/test/test_tutorials.py deleted file mode 100644 index aab2786..0000000 --- a/libs/cherrypy/test/test_tutorials.py +++ /dev/null @@ -1,201 +0,0 @@ -import sys - -import cherrypy -from cherrypy.test import helper - - -class TutorialTest(helper.CPWebCase): - - def setup_server(cls): - - conf = cherrypy.config.copy() - - def load_tut_module(name): - """Import or reload tutorial module as needed.""" - cherrypy.config.reset() - cherrypy.config.update(conf) - - target = "cherrypy.tutorial." + name - if target in sys.modules: - module = reload(sys.modules[target]) - else: - module = __import__(target, globals(), locals(), ['']) - # The above import will probably mount a new app at "". - app = cherrypy.tree.apps[""] - - app.root.load_tut_module = load_tut_module - app.root.sessions = sessions - app.root.traceback_setting = traceback_setting - - cls.supervisor.sync_apps() - load_tut_module.exposed = True - - def sessions(): - cherrypy.config.update({"tools.sessions.on": True}) - sessions.exposed = True - - def traceback_setting(): - return repr(cherrypy.request.show_tracebacks) - traceback_setting.exposed = True - - class Dummy: - pass - root = Dummy() - root.load_tut_module = load_tut_module - cherrypy.tree.mount(root) - setup_server = classmethod(setup_server) - - - def test01HelloWorld(self): - self.getPage("/load_tut_module/tut01_helloworld") - self.getPage("/") - self.assertBody('Hello world!') - - def test02ExposeMethods(self): - self.getPage("/load_tut_module/tut02_expose_methods") - self.getPage("/showMessage") - self.assertBody('Hello world!') - - def test03GetAndPost(self): - self.getPage("/load_tut_module/tut03_get_and_post") - - # Try different GET queries - self.getPage("/greetUser?name=Bob") - self.assertBody("Hey Bob, what's up?") - - self.getPage("/greetUser") - self.assertBody('Please enter your name here.') - - self.getPage("/greetUser?name=") - self.assertBody('No, really, enter your name here.') - - # Try the same with POST - self.getPage("/greetUser", method="POST", body="name=Bob") - self.assertBody("Hey Bob, what's up?") - - self.getPage("/greetUser", method="POST", body="name=") - self.assertBody('No, really, enter your name here.') - - def test04ComplexSite(self): - self.getPage("/load_tut_module/tut04_complex_site") - msg = ''' -

Here are some extra useful links:

- - - -

[Return to links page]

''' - self.getPage("/links/extra/") - self.assertBody(msg) - - def test05DerivedObjects(self): - self.getPage("/load_tut_module/tut05_derived_objects") - msg = ''' - - - Another Page - - -

Another Page

- -

- And this is the amazing second page! -

- - - - ''' - self.getPage("/another/") - self.assertBody(msg) - - def test06DefaultMethod(self): - self.getPage("/load_tut_module/tut06_default_method") - self.getPage('/hendrik') - self.assertBody('Hendrik Mans, CherryPy co-developer & crazy German ' - '(back)') - - def test07Sessions(self): - self.getPage("/load_tut_module/tut07_sessions") - self.getPage("/sessions") - - self.getPage('/') - self.assertBody("\n During your current session, you've viewed this" - "\n page 1 times! Your life is a patio of fun!" - "\n ") - - self.getPage('/', self.cookies) - self.assertBody("\n During your current session, you've viewed this" - "\n page 2 times! Your life is a patio of fun!" - "\n ") - - def test08GeneratorsAndYield(self): - self.getPage("/load_tut_module/tut08_generators_and_yield") - self.getPage('/') - self.assertBody('

Generators rule!

' - '

List of users:

' - 'Remi
Carlos
Hendrik
Lorenzo Lamas
' - '') - - def test09Files(self): - self.getPage("/load_tut_module/tut09_files") - - # Test upload - filesize = 5 - h = [("Content-type", "multipart/form-data; boundary=x"), - ("Content-Length", str(105 + filesize))] - b = '--x\n' + \ - 'Content-Disposition: form-data; name="myFile"; filename="hello.txt"\r\n' + \ - 'Content-Type: text/plain\r\n' + \ - '\r\n' + \ - 'a' * filesize + '\n' + \ - '--x--\n' - self.getPage('/upload', h, "POST", b) - self.assertBody(''' - - myFile length: %d
- myFile filename: hello.txt
- myFile mime-type: text/plain - - ''' % filesize) - - # Test download - self.getPage('/download') - self.assertStatus("200 OK") - self.assertHeader("Content-Type", "application/x-download") - self.assertHeader("Content-Disposition", - # Make sure the filename is quoted. - 'attachment; filename="pdf_file.pdf"') - self.assertEqual(len(self.body), 85698) - - def test10HTTPErrors(self): - self.getPage("/load_tut_module/tut10_http_errors") - - self.getPage("/") - self.assertInBody("""""") - self.assertInBody("""""") - self.assertInBody("""""") - self.assertInBody("""""") - self.assertInBody("""""") - - self.getPage("/traceback_setting") - setting = self.body - self.getPage("/toggleTracebacks") - self.assertStatus((302, 303)) - self.getPage("/traceback_setting") - self.assertBody(str(not eval(setting))) - - self.getPage("/error?code=500") - self.assertStatus(500) - self.assertInBody("The server encountered an unexpected condition " - "which prevented it from fulfilling the request.") - - self.getPage("/error?code=403") - self.assertStatus(403) - self.assertInBody("

You can't do that!

") - - self.getPage("/messageArg") - self.assertStatus(500) - self.assertInBody("If you construct an HTTPError with a 'message'") - diff --git a/libs/cherrypy/test/test_virtualhost.py b/libs/cherrypy/test/test_virtualhost.py deleted file mode 100644 index dbd2dbc..0000000 --- a/libs/cherrypy/test/test_virtualhost.py +++ /dev/null @@ -1,107 +0,0 @@ -import os -curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - -import cherrypy -from cherrypy.test import helper - - -class VirtualHostTest(helper.CPWebCase): - - def setup_server(): - class Root: - def index(self): - return "Hello, world" - index.exposed = True - - def dom4(self): - return "Under construction" - dom4.exposed = True - - def method(self, value): - return "You sent %s" % value - method.exposed = True - - class VHost: - def __init__(self, sitename): - self.sitename = sitename - - def index(self): - return "Welcome to %s" % self.sitename - index.exposed = True - - def vmethod(self, value): - return "You sent %s" % value - vmethod.exposed = True - - def url(self): - return cherrypy.url("nextpage") - url.exposed = True - - # Test static as a handler (section must NOT include vhost prefix) - static = cherrypy.tools.staticdir.handler(section='/static', dir=curdir) - - root = Root() - root.mydom2 = VHost("Domain 2") - root.mydom3 = VHost("Domain 3") - hostmap = {'www.mydom2.com': '/mydom2', - 'www.mydom3.com': '/mydom3', - 'www.mydom4.com': '/dom4', - } - cherrypy.tree.mount(root, config={ - '/': {'request.dispatch': cherrypy.dispatch.VirtualHost(**hostmap)}, - # Test static in config (section must include vhost prefix) - '/mydom2/static2': {'tools.staticdir.on': True, - 'tools.staticdir.root': curdir, - 'tools.staticdir.dir': 'static', - 'tools.staticdir.index': 'index.html', - }, - }) - setup_server = staticmethod(setup_server) - - def testVirtualHost(self): - self.getPage("/", [('Host', 'www.mydom1.com')]) - self.assertBody('Hello, world') - self.getPage("/mydom2/", [('Host', 'www.mydom1.com')]) - self.assertBody('Welcome to Domain 2') - - self.getPage("/", [('Host', 'www.mydom2.com')]) - self.assertBody('Welcome to Domain 2') - self.getPage("/", [('Host', 'www.mydom3.com')]) - self.assertBody('Welcome to Domain 3') - self.getPage("/", [('Host', 'www.mydom4.com')]) - self.assertBody('Under construction') - - # Test GET, POST, and positional params - self.getPage("/method?value=root") - self.assertBody("You sent root") - self.getPage("/vmethod?value=dom2+GET", [('Host', 'www.mydom2.com')]) - self.assertBody("You sent dom2 GET") - self.getPage("/vmethod", [('Host', 'www.mydom3.com')], method="POST", - body="value=dom3+POST") - self.assertBody("You sent dom3 POST") - self.getPage("/vmethod/pos", [('Host', 'www.mydom3.com')]) - self.assertBody("You sent pos") - - # Test that cherrypy.url uses the browser url, not the virtual url - self.getPage("/url", [('Host', 'www.mydom2.com')]) - self.assertBody("%s://www.mydom2.com/nextpage" % self.scheme) - - def test_VHost_plus_Static(self): - # Test static as a handler - self.getPage("/static/style.css", [('Host', 'www.mydom2.com')]) - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/css;charset=utf-8') - - # Test static in config - self.getPage("/static2/dirback.jpg", [('Host', 'www.mydom2.com')]) - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'image/jpeg') - - # Test static config with "index" arg - self.getPage("/static2/", [('Host', 'www.mydom2.com')]) - self.assertStatus('200 OK') - self.assertBody('Hello, world\r\n') - # Since tools.trailing_slash is on by default, this should redirect - self.getPage("/static2", [('Host', 'www.mydom2.com')]) - self.assertStatus(301) - diff --git a/libs/cherrypy/test/test_wsgi_ns.py b/libs/cherrypy/test/test_wsgi_ns.py deleted file mode 100644 index e3c6ce6..0000000 --- a/libs/cherrypy/test/test_wsgi_ns.py +++ /dev/null @@ -1,91 +0,0 @@ -import cherrypy -from cherrypy._cpcompat import ntob -from cherrypy.test import helper - - -class WSGI_Namespace_Test(helper.CPWebCase): - - def setup_server(): - - class WSGIResponse(object): - - def __init__(self, appresults): - self.appresults = appresults - self.iter = iter(appresults) - - def __iter__(self): - return self - - def next(self): - return self.iter.next() - def __next__(self): - return next(self.iter) - - def close(self): - if hasattr(self.appresults, "close"): - self.appresults.close() - - - class ChangeCase(object): - - def __init__(self, app, to=None): - self.app = app - self.to = to - - def __call__(self, environ, start_response): - res = self.app(environ, start_response) - class CaseResults(WSGIResponse): - def next(this): - return getattr(this.iter.next(), self.to)() - def __next__(this): - return getattr(next(this.iter), self.to)() - return CaseResults(res) - - class Replacer(object): - - def __init__(self, app, map={}): - self.app = app - self.map = map - - def __call__(self, environ, start_response): - res = self.app(environ, start_response) - class ReplaceResults(WSGIResponse): - def next(this): - line = this.iter.next() - for k, v in self.map.iteritems(): - line = line.replace(k, v) - return line - def __next__(this): - line = next(this.iter) - for k, v in self.map.items(): - line = line.replace(k, v) - return line - return ReplaceResults(res) - - class Root(object): - - def index(self): - return "HellO WoRlD!" - index.exposed = True - - - root_conf = {'wsgi.pipeline': [('replace', Replacer)], - 'wsgi.replace.map': {ntob('L'): ntob('X'), - ntob('l'): ntob('r')}, - } - - app = cherrypy.Application(Root()) - app.wsgiapp.pipeline.append(('changecase', ChangeCase)) - app.wsgiapp.config['changecase'] = {'to': 'upper'} - cherrypy.tree.mount(app, config={'/': root_conf}) - setup_server = staticmethod(setup_server) - - - def test_pipeline(self): - if not cherrypy.server.httpserver: - return self.skip() - - self.getPage("/") - # If body is "HEXXO WORXD!", the middleware was applied out of order. - self.assertBody("HERRO WORRD!") - diff --git a/libs/cherrypy/test/test_wsgi_vhost.py b/libs/cherrypy/test/test_wsgi_vhost.py deleted file mode 100644 index abb1a91..0000000 --- a/libs/cherrypy/test/test_wsgi_vhost.py +++ /dev/null @@ -1,36 +0,0 @@ -import cherrypy -from cherrypy.test import helper - - -class WSGI_VirtualHost_Test(helper.CPWebCase): - - def setup_server(): - - class ClassOfRoot(object): - - def __init__(self, name): - self.name = name - - def index(self): - return "Welcome to the %s website!" % self.name - index.exposed = True - - - default = cherrypy.Application(None) - - domains = {} - for year in range(1997, 2008): - app = cherrypy.Application(ClassOfRoot('Class of %s' % year)) - domains['www.classof%s.example' % year] = app - - cherrypy.tree.graft(cherrypy._cpwsgi.VirtualHost(default, domains)) - setup_server = staticmethod(setup_server) - - def test_welcome(self): - if not cherrypy.server.using_wsgi: - return self.skip("skipped (not using WSGI)... ") - - for year in range(1997, 2008): - self.getPage("/", headers=[('Host', 'www.classof%s.example' % year)]) - self.assertBody("Welcome to the Class of %s website!" % year) - diff --git a/libs/cherrypy/test/test_wsgiapps.py b/libs/cherrypy/test/test_wsgiapps.py deleted file mode 100644 index d4b8b79..0000000 --- a/libs/cherrypy/test/test_wsgiapps.py +++ /dev/null @@ -1,118 +0,0 @@ -from cherrypy._cpcompat import ntob -from cherrypy.test import helper - - -class WSGIGraftTests(helper.CPWebCase): - - def setup_server(): - import os - curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - - import cherrypy - - def test_app(environ, start_response): - status = '200 OK' - response_headers = [('Content-type', 'text/plain')] - start_response(status, response_headers) - output = ['Hello, world!\n', - 'This is a wsgi app running within CherryPy!\n\n'] - keys = list(environ.keys()) - keys.sort() - for k in keys: - output.append('%s: %s\n' % (k,environ[k])) - return [ntob(x, 'utf-8') for x in output] - - def test_empty_string_app(environ, start_response): - status = '200 OK' - response_headers = [('Content-type', 'text/plain')] - start_response(status, response_headers) - return [ntob('Hello'), ntob(''), ntob(' '), ntob(''), ntob('world')] - - - class WSGIResponse(object): - - def __init__(self, appresults): - self.appresults = appresults - self.iter = iter(appresults) - - def __iter__(self): - return self - - def next(self): - return self.iter.next() - def __next__(self): - return next(self.iter) - - def close(self): - if hasattr(self.appresults, "close"): - self.appresults.close() - - - class ReversingMiddleware(object): - - def __init__(self, app): - self.app = app - - def __call__(self, environ, start_response): - results = app(environ, start_response) - class Reverser(WSGIResponse): - def next(this): - line = list(this.iter.next()) - line.reverse() - return "".join(line) - def __next__(this): - line = list(next(this.iter)) - line.reverse() - return bytes(line) - return Reverser(results) - - class Root: - def index(self): - return ntob("I'm a regular CherryPy page handler!") - index.exposed = True - - - cherrypy.tree.mount(Root()) - - cherrypy.tree.graft(test_app, '/hosted/app1') - cherrypy.tree.graft(test_empty_string_app, '/hosted/app3') - - # Set script_name explicitly to None to signal CP that it should - # be pulled from the WSGI environ each time. - app = cherrypy.Application(Root(), script_name=None) - cherrypy.tree.graft(ReversingMiddleware(app), '/hosted/app2') - setup_server = staticmethod(setup_server) - - wsgi_output = '''Hello, world! -This is a wsgi app running within CherryPy!''' - - def test_01_standard_app(self): - self.getPage("/") - self.assertBody("I'm a regular CherryPy page handler!") - - def test_04_pure_wsgi(self): - import cherrypy - if not cherrypy.server.using_wsgi: - return self.skip("skipped (not using WSGI)... ") - self.getPage("/hosted/app1") - self.assertHeader("Content-Type", "text/plain") - self.assertInBody(self.wsgi_output) - - def test_05_wrapped_cp_app(self): - import cherrypy - if not cherrypy.server.using_wsgi: - return self.skip("skipped (not using WSGI)... ") - self.getPage("/hosted/app2/") - body = list("I'm a regular CherryPy page handler!") - body.reverse() - body = "".join(body) - self.assertInBody(body) - - def test_06_empty_string_app(self): - import cherrypy - if not cherrypy.server.using_wsgi: - return self.skip("skipped (not using WSGI)... ") - self.getPage("/hosted/app3") - self.assertHeader("Content-Type", "text/plain") - self.assertInBody('Hello world') - diff --git a/libs/cherrypy/test/test_xmlrpc.py b/libs/cherrypy/test/test_xmlrpc.py deleted file mode 100644 index f7a6927..0000000 --- a/libs/cherrypy/test/test_xmlrpc.py +++ /dev/null @@ -1,179 +0,0 @@ -import sys -from cherrypy._cpcompat import py3k - -try: - from xmlrpclib import DateTime, Fault, ProtocolError, ServerProxy, SafeTransport -except ImportError: - from xmlrpc.client import DateTime, Fault, ProtocolError, ServerProxy, SafeTransport - -if py3k: - HTTPSTransport = SafeTransport - - # Python 3.0's SafeTransport still mistakenly checks for socket.ssl - import socket - if not hasattr(socket, "ssl"): - socket.ssl = True -else: - class HTTPSTransport(SafeTransport): - """Subclass of SafeTransport to fix sock.recv errors (by using file).""" - - def request(self, host, handler, request_body, verbose=0): - # issue XML-RPC request - h = self.make_connection(host) - if verbose: - h.set_debuglevel(1) - - self.send_request(h, handler, request_body) - self.send_host(h, host) - self.send_user_agent(h) - self.send_content(h, request_body) - - errcode, errmsg, headers = h.getreply() - if errcode != 200: - raise ProtocolError(host + handler, errcode, errmsg, headers) - - self.verbose = verbose - - # Here's where we differ from the superclass. It says: - # try: - # sock = h._conn.sock - # except AttributeError: - # sock = None - # return self._parse_response(h.getfile(), sock) - - return self.parse_response(h.getfile()) - -import cherrypy - - -def setup_server(): - from cherrypy import _cptools - - class Root: - def index(self): - return "I'm a standard index!" - index.exposed = True - - - class XmlRpc(_cptools.XMLRPCController): - - def foo(self): - return "Hello world!" - foo.exposed = True - - def return_single_item_list(self): - return [42] - return_single_item_list.exposed = True - - def return_string(self): - return "here is a string" - return_string.exposed = True - - def return_tuple(self): - return ('here', 'is', 1, 'tuple') - return_tuple.exposed = True - - def return_dict(self): - return dict(a=1, b=2, c=3) - return_dict.exposed = True - - def return_composite(self): - return dict(a=1,z=26), 'hi', ['welcome', 'friend'] - return_composite.exposed = True - - def return_int(self): - return 42 - return_int.exposed = True - - def return_float(self): - return 3.14 - return_float.exposed = True - - def return_datetime(self): - return DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1)) - return_datetime.exposed = True - - def return_boolean(self): - return True - return_boolean.exposed = True - - def test_argument_passing(self, num): - return num * 2 - test_argument_passing.exposed = True - - def test_returning_Fault(self): - return Fault(1, "custom Fault response") - test_returning_Fault.exposed = True - - root = Root() - root.xmlrpc = XmlRpc() - cherrypy.tree.mount(root, config={'/': { - 'request.dispatch': cherrypy.dispatch.XMLRPCDispatcher(), - 'tools.xmlrpc.allow_none': 0, - }}) - - -from cherrypy.test import helper - -class XmlRpcTest(helper.CPWebCase): - setup_server = staticmethod(setup_server) - def testXmlRpc(self): - - scheme = self.scheme - if scheme == "https": - url = 'https://%s:%s/xmlrpc/' % (self.interface(), self.PORT) - proxy = ServerProxy(url, transport=HTTPSTransport()) - else: - url = 'http://%s:%s/xmlrpc/' % (self.interface(), self.PORT) - proxy = ServerProxy(url) - - # begin the tests ... - self.getPage("/xmlrpc/foo") - self.assertBody("Hello world!") - - self.assertEqual(proxy.return_single_item_list(), [42]) - self.assertNotEqual(proxy.return_single_item_list(), 'one bazillion') - self.assertEqual(proxy.return_string(), "here is a string") - self.assertEqual(proxy.return_tuple(), list(('here', 'is', 1, 'tuple'))) - self.assertEqual(proxy.return_dict(), {'a': 1, 'c': 3, 'b': 2}) - self.assertEqual(proxy.return_composite(), - [{'a': 1, 'z': 26}, 'hi', ['welcome', 'friend']]) - self.assertEqual(proxy.return_int(), 42) - self.assertEqual(proxy.return_float(), 3.14) - self.assertEqual(proxy.return_datetime(), - DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1))) - self.assertEqual(proxy.return_boolean(), True) - self.assertEqual(proxy.test_argument_passing(22), 22 * 2) - - # Test an error in the page handler (should raise an xmlrpclib.Fault) - try: - proxy.test_argument_passing({}) - except Exception: - x = sys.exc_info()[1] - self.assertEqual(x.__class__, Fault) - self.assertEqual(x.faultString, ("unsupported operand type(s) " - "for *: 'dict' and 'int'")) - else: - self.fail("Expected xmlrpclib.Fault") - - # http://www.cherrypy.org/ticket/533 - # if a method is not found, an xmlrpclib.Fault should be raised - try: - proxy.non_method() - except Exception: - x = sys.exc_info()[1] - self.assertEqual(x.__class__, Fault) - self.assertEqual(x.faultString, 'method "non_method" is not supported') - else: - self.fail("Expected xmlrpclib.Fault") - - # Test returning a Fault from the page handler. - try: - proxy.test_returning_Fault() - except Exception: - x = sys.exc_info()[1] - self.assertEqual(x.__class__, Fault) - self.assertEqual(x.faultString, ("custom Fault response")) - else: - self.fail("Expected xmlrpclib.Fault") - diff --git a/libs/cherrypy/test/webtest.py b/libs/cherrypy/test/webtest.py deleted file mode 100644 index 50cfbad..0000000 --- a/libs/cherrypy/test/webtest.py +++ /dev/null @@ -1,575 +0,0 @@ -"""Extensions to unittest for web frameworks. - -Use the WebCase.getPage method to request a page from your HTTP server. - -Framework Integration -===================== - -If you have control over your server process, you can handle errors -in the server-side of the HTTP conversation a bit better. You must run -both the client (your WebCase tests) and the server in the same process -(but in separate threads, obviously). - -When an error occurs in the framework, call server_error. It will print -the traceback to stdout, and keep any assertions you have from running -(the assumption is that, if the server errors, the page output will not -be of further significance to your tests). -""" - -import os -import pprint -import re -import socket -import sys -import time -import traceback -import types - -from unittest import * -from unittest import _TextTestResult - -from cherrypy._cpcompat import basestring, ntob, py3k, HTTPConnection, HTTPSConnection, unicodestr - - - -def interface(host): - """Return an IP address for a client connection given the server host. - - If the server is listening on '0.0.0.0' (INADDR_ANY) - or '::' (IN6ADDR_ANY), this will return the proper localhost.""" - if host == '0.0.0.0': - # INADDR_ANY, which should respond on localhost. - return "127.0.0.1" - if host == '::': - # IN6ADDR_ANY, which should respond on localhost. - return "::1" - return host - - -class TerseTestResult(_TextTestResult): - - def printErrors(self): - # Overridden to avoid unnecessary empty line - if self.errors or self.failures: - if self.dots or self.showAll: - self.stream.writeln() - self.printErrorList('ERROR', self.errors) - self.printErrorList('FAIL', self.failures) - - -class TerseTestRunner(TextTestRunner): - """A test runner class that displays results in textual form.""" - - def _makeResult(self): - return TerseTestResult(self.stream, self.descriptions, self.verbosity) - - def run(self, test): - "Run the given test case or test suite." - # Overridden to remove unnecessary empty lines and separators - result = self._makeResult() - test(result) - result.printErrors() - if not result.wasSuccessful(): - self.stream.write("FAILED (") - failed, errored = list(map(len, (result.failures, result.errors))) - if failed: - self.stream.write("failures=%d" % failed) - if errored: - if failed: self.stream.write(", ") - self.stream.write("errors=%d" % errored) - self.stream.writeln(")") - return result - - -class ReloadingTestLoader(TestLoader): - - def loadTestsFromName(self, name, module=None): - """Return a suite of all tests cases given a string specifier. - - The name may resolve either to a module, a test case class, a - test method within a test case class, or a callable object which - returns a TestCase or TestSuite instance. - - The method optionally resolves the names relative to a given module. - """ - parts = name.split('.') - unused_parts = [] - if module is None: - if not parts: - raise ValueError("incomplete test name: %s" % name) - else: - parts_copy = parts[:] - while parts_copy: - target = ".".join(parts_copy) - if target in sys.modules: - module = reload(sys.modules[target]) - parts = unused_parts - break - else: - try: - module = __import__(target) - parts = unused_parts - break - except ImportError: - unused_parts.insert(0,parts_copy[-1]) - del parts_copy[-1] - if not parts_copy: - raise - parts = parts[1:] - obj = module - for part in parts: - obj = getattr(obj, part) - - if type(obj) == types.ModuleType: - return self.loadTestsFromModule(obj) - elif (((py3k and isinstance(obj, type)) - or isinstance(obj, (type, types.ClassType))) - and issubclass(obj, TestCase)): - return self.loadTestsFromTestCase(obj) - elif type(obj) == types.UnboundMethodType: - if py3k: - return obj.__self__.__class__(obj.__name__) - else: - return obj.im_class(obj.__name__) - elif hasattr(obj, '__call__'): - test = obj() - if not isinstance(test, TestCase) and \ - not isinstance(test, TestSuite): - raise ValueError("calling %s returned %s, " - "not a test" % (obj,test)) - return test - else: - raise ValueError("do not know how to make test from: %s" % obj) - - -try: - # Jython support - if sys.platform[:4] == 'java': - def getchar(): - # Hopefully this is enough - return sys.stdin.read(1) - else: - # On Windows, msvcrt.getch reads a single char without output. - import msvcrt - def getchar(): - return msvcrt.getch() -except ImportError: - # Unix getchr - import tty, termios - def getchar(): - fd = sys.stdin.fileno() - old_settings = termios.tcgetattr(fd) - try: - tty.setraw(sys.stdin.fileno()) - ch = sys.stdin.read(1) - finally: - termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) - return ch - - -class WebCase(TestCase): - HOST = "127.0.0.1" - PORT = 8000 - HTTP_CONN = HTTPConnection - PROTOCOL = "HTTP/1.1" - - scheme = "http" - url = None - - status = None - headers = None - body = None - - encoding = 'utf-8' - - time = None - - def get_conn(self, auto_open=False): - """Return a connection to our HTTP server.""" - if self.scheme == "https": - cls = HTTPSConnection - else: - cls = HTTPConnection - conn = cls(self.interface(), self.PORT) - # Automatically re-connect? - conn.auto_open = auto_open - conn.connect() - return conn - - def set_persistent(self, on=True, auto_open=False): - """Make our HTTP_CONN persistent (or not). - - If the 'on' argument is True (the default), then self.HTTP_CONN - will be set to an instance of HTTPConnection (or HTTPS - if self.scheme is "https"). This will then persist across requests. - - We only allow for a single open connection, so if you call this - and we currently have an open connection, it will be closed. - """ - try: - self.HTTP_CONN.close() - except (TypeError, AttributeError): - pass - - if on: - self.HTTP_CONN = self.get_conn(auto_open=auto_open) - else: - if self.scheme == "https": - self.HTTP_CONN = HTTPSConnection - else: - self.HTTP_CONN = HTTPConnection - - def _get_persistent(self): - return hasattr(self.HTTP_CONN, "__class__") - def _set_persistent(self, on): - self.set_persistent(on) - persistent = property(_get_persistent, _set_persistent) - - def interface(self): - """Return an IP address for a client connection. - - If the server is listening on '0.0.0.0' (INADDR_ANY) - or '::' (IN6ADDR_ANY), this will return the proper localhost.""" - return interface(self.HOST) - - def getPage(self, url, headers=None, method="GET", body=None, protocol=None): - """Open the url with debugging support. Return status, headers, body.""" - ServerError.on = False - - if isinstance(url, unicodestr): - url = url.encode('utf-8') - if isinstance(body, unicodestr): - body = body.encode('utf-8') - - self.url = url - self.time = None - start = time.time() - result = openURL(url, headers, method, body, self.HOST, self.PORT, - self.HTTP_CONN, protocol or self.PROTOCOL) - self.time = time.time() - start - self.status, self.headers, self.body = result - - # Build a list of request cookies from the previous response cookies. - self.cookies = [('Cookie', v) for k, v in self.headers - if k.lower() == 'set-cookie'] - - if ServerError.on: - raise ServerError() - return result - - interactive = True - console_height = 30 - - def _handlewebError(self, msg): - print("") - print(" ERROR: %s" % msg) - - if not self.interactive: - raise self.failureException(msg) - - p = " Show: [B]ody [H]eaders [S]tatus [U]RL; [I]gnore, [R]aise, or sys.e[X]it >> " - sys.stdout.write(p) - sys.stdout.flush() - while True: - i = getchar().upper() - if not isinstance(i, type("")): - i = i.decode('ascii') - if i not in "BHSUIRX": - continue - print(i.upper()) # Also prints new line - if i == "B": - for x, line in enumerate(self.body.splitlines()): - if (x + 1) % self.console_height == 0: - # The \r and comma should make the next line overwrite - sys.stdout.write("<-- More -->\r") - m = getchar().lower() - # Erase our "More" prompt - sys.stdout.write(" \r") - if m == "q": - break - print(line) - elif i == "H": - pprint.pprint(self.headers) - elif i == "S": - print(self.status) - elif i == "U": - print(self.url) - elif i == "I": - # return without raising the normal exception - return - elif i == "R": - raise self.failureException(msg) - elif i == "X": - self.exit() - sys.stdout.write(p) - sys.stdout.flush() - - def exit(self): - sys.exit() - - def assertStatus(self, status, msg=None): - """Fail if self.status != status.""" - if isinstance(status, basestring): - if not self.status == status: - if msg is None: - msg = 'Status (%r) != %r' % (self.status, status) - self._handlewebError(msg) - elif isinstance(status, int): - code = int(self.status[:3]) - if code != status: - if msg is None: - msg = 'Status (%r) != %r' % (self.status, status) - self._handlewebError(msg) - else: - # status is a tuple or list. - match = False - for s in status: - if isinstance(s, basestring): - if self.status == s: - match = True - break - elif int(self.status[:3]) == s: - match = True - break - if not match: - if msg is None: - msg = 'Status (%r) not in %r' % (self.status, status) - self._handlewebError(msg) - - def assertHeader(self, key, value=None, msg=None): - """Fail if (key, [value]) not in self.headers.""" - lowkey = key.lower() - for k, v in self.headers: - if k.lower() == lowkey: - if value is None or str(value) == v: - return v - - if msg is None: - if value is None: - msg = '%r not in headers' % key - else: - msg = '%r:%r not in headers' % (key, value) - self._handlewebError(msg) - - def assertHeaderItemValue(self, key, value, msg=None): - """Fail if the header does not contain the specified value""" - actual_value = self.assertHeader(key, msg=msg) - header_values = map(str.strip, actual_value.split(',')) - if value in header_values: - return value - - if msg is None: - msg = "%r not in %r" % (value, header_values) - self._handlewebError(msg) - - def assertNoHeader(self, key, msg=None): - """Fail if key in self.headers.""" - lowkey = key.lower() - matches = [k for k, v in self.headers if k.lower() == lowkey] - if matches: - if msg is None: - msg = '%r in headers' % key - self._handlewebError(msg) - - def assertBody(self, value, msg=None): - """Fail if value != self.body.""" - if isinstance(value, unicodestr): - value = value.encode(self.encoding) - if value != self.body: - if msg is None: - msg = 'expected body:\n%r\n\nactual body:\n%r' % (value, self.body) - self._handlewebError(msg) - - def assertInBody(self, value, msg=None): - """Fail if value not in self.body.""" - if isinstance(value, unicodestr): - value = value.encode(self.encoding) - if value not in self.body: - if msg is None: - msg = '%r not in body: %s' % (value, self.body) - self._handlewebError(msg) - - def assertNotInBody(self, value, msg=None): - """Fail if value in self.body.""" - if isinstance(value, unicodestr): - value = value.encode(self.encoding) - if value in self.body: - if msg is None: - msg = '%r found in body' % value - self._handlewebError(msg) - - def assertMatchesBody(self, pattern, msg=None, flags=0): - """Fail if value (a regex pattern) is not in self.body.""" - if isinstance(pattern, unicodestr): - pattern = pattern.encode(self.encoding) - if re.search(pattern, self.body, flags) is None: - if msg is None: - msg = 'No match for %r in body' % pattern - self._handlewebError(msg) - - -methods_with_bodies = ("POST", "PUT") - -def cleanHeaders(headers, method, body, host, port): - """Return request headers, with required headers added (if missing).""" - if headers is None: - headers = [] - - # Add the required Host request header if not present. - # [This specifies the host:port of the server, not the client.] - found = False - for k, v in headers: - if k.lower() == 'host': - found = True - break - if not found: - if port == 80: - headers.append(("Host", host)) - else: - headers.append(("Host", "%s:%s" % (host, port))) - - if method in methods_with_bodies: - # Stick in default type and length headers if not present - found = False - for k, v in headers: - if k.lower() == 'content-type': - found = True - break - if not found: - headers.append(("Content-Type", "application/x-www-form-urlencoded")) - headers.append(("Content-Length", str(len(body or "")))) - - return headers - - -def shb(response): - """Return status, headers, body the way we like from a response.""" - if py3k: - h = response.getheaders() - else: - h = [] - key, value = None, None - for line in response.msg.headers: - if line: - if line[0] in " \t": - value += line.strip() - else: - if key and value: - h.append((key, value)) - key, value = line.split(":", 1) - key = key.strip() - value = value.strip() - if key and value: - h.append((key, value)) - - return "%s %s" % (response.status, response.reason), h, response.read() - - -def openURL(url, headers=None, method="GET", body=None, - host="127.0.0.1", port=8000, http_conn=HTTPConnection, - protocol="HTTP/1.1"): - """Open the given HTTP resource and return status, headers, and body.""" - - headers = cleanHeaders(headers, method, body, host, port) - - # Trying 10 times is simply in case of socket errors. - # Normal case--it should run once. - for trial in range(10): - try: - # Allow http_conn to be a class or an instance - if hasattr(http_conn, "host"): - conn = http_conn - else: - conn = http_conn(interface(host), port) - - conn._http_vsn_str = protocol - conn._http_vsn = int("".join([x for x in protocol if x.isdigit()])) - - # skip_accept_encoding argument added in python version 2.4 - if sys.version_info < (2, 4): - def putheader(self, header, value): - if header == 'Accept-Encoding' and value == 'identity': - return - self.__class__.putheader(self, header, value) - import new - conn.putheader = new.instancemethod(putheader, conn, conn.__class__) - conn.putrequest(method.upper(), url, skip_host=True) - elif not py3k: - conn.putrequest(method.upper(), url, skip_host=True, - skip_accept_encoding=True) - else: - import http.client - # Replace the stdlib method, which only accepts ASCII url's - def putrequest(self, method, url): - if self._HTTPConnection__response and self._HTTPConnection__response.isclosed(): - self._HTTPConnection__response = None - - if self._HTTPConnection__state == http.client._CS_IDLE: - self._HTTPConnection__state = http.client._CS_REQ_STARTED - else: - raise http.client.CannotSendRequest() - - self._method = method - if not url: - url = ntob('/') - request = ntob(' ').join((method.encode("ASCII"), url, - self._http_vsn_str.encode("ASCII"))) - self._output(request) - import types - conn.putrequest = types.MethodType(putrequest, conn) - - conn.putrequest(method.upper(), url) - - for key, value in headers: - conn.putheader(key, ntob(value, "Latin-1")) - conn.endheaders() - - if body is not None: - conn.send(body) - - # Handle response - response = conn.getresponse() - - s, h, b = shb(response) - - if not hasattr(http_conn, "host"): - # We made our own conn instance. Close it. - conn.close() - - return s, h, b - except socket.error: - time.sleep(0.5) - if trial == 9: - raise - - -# Add any exceptions which your web framework handles -# normally (that you don't want server_error to trap). -ignored_exceptions = [] - -# You'll want set this to True when you can't guarantee -# that each response will immediately follow each request; -# for example, when handling requests via multiple threads. -ignore_all = False - -class ServerError(Exception): - on = False - - -def server_error(exc=None): - """Server debug hook. Return True if exception handled, False if ignored. - - You probably want to wrap this, so you can still handle an error using - your framework when it's ignored. - """ - if exc is None: - exc = sys.exc_info() - - if ignore_all or exc[0] in ignored_exceptions: - return False - else: - ServerError.on = True - print("") - print("".join(traceback.format_exception(*exc))) - return True - diff --git a/libs/cherrypy/tutorial/README.txt b/libs/cherrypy/tutorial/README.txt deleted file mode 100644 index 2b877e1..0000000 --- a/libs/cherrypy/tutorial/README.txt +++ /dev/null @@ -1,16 +0,0 @@ -CherryPy Tutorials ------------------------------------------------------------------------- - -This is a series of tutorials explaining how to develop dynamic web -applications using CherryPy. A couple of notes: - - - Each of these tutorials builds on the ones before it. If you're - new to CherryPy, we recommend you start with 01_helloworld.py and - work your way upwards. :) - - - In most of these tutorials, you will notice that all output is done - by returning normal Python strings, often using simple Python - variable substitution. In most real-world applications, you will - probably want to use a separate template package (like Cheetah, - CherryTemplate or XML/XSL). - diff --git a/libs/cherrypy/tutorial/__init__.py b/libs/cherrypy/tutorial/__init__.py deleted file mode 100644 index c4e2c55..0000000 --- a/libs/cherrypy/tutorial/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ - -# This is used in test_config to test unrepr of "from A import B" -thing2 = object() \ No newline at end of file diff --git a/libs/cherrypy/tutorial/bonus-sqlobject.py b/libs/cherrypy/tutorial/bonus-sqlobject.py deleted file mode 100644 index c43feb4..0000000 --- a/libs/cherrypy/tutorial/bonus-sqlobject.py +++ /dev/null @@ -1,168 +0,0 @@ -''' -Bonus Tutorial: Using SQLObject - -This is a silly little contacts manager application intended to -demonstrate how to use SQLObject from within a CherryPy2 project. It -also shows how to use inline Cheetah templates. - -SQLObject is an Object/Relational Mapper that allows you to access -data stored in an RDBMS in a pythonic fashion. You create data objects -as Python classes and let SQLObject take care of all the nasty details. - -This code depends on the latest development version (0.6+) of SQLObject. -You can get it from the SQLObject Subversion server. You can find all -necessary information at . This code will NOT -work with the 0.5.x version advertised on their website! - -This code also depends on a recent version of Cheetah. You can find -Cheetah at . - -After starting this application for the first time, you will need to -access the /reset URI in order to create the database table and some -sample data. Accessing /reset again will drop and re-create the table, -so you may want to be careful. :-) - -This application isn't supposed to be fool-proof, it's not even supposed -to be very GOOD. Play around with it some, browse the source code, smile. - -:) - --- Hendrik Mans -''' - -import cherrypy -from Cheetah.Template import Template -from sqlobject import * - -# configure your database connection here -__connection__ = 'mysql://root:@localhost/test' - -# this is our (only) data class. -class Contact(SQLObject): - lastName = StringCol(length = 50, notNone = True) - firstName = StringCol(length = 50, notNone = True) - phone = StringCol(length = 30, notNone = True, default = '') - email = StringCol(length = 30, notNone = True, default = '') - url = StringCol(length = 100, notNone = True, default = '') - - -class ContactManager: - def index(self): - # Let's display a list of all stored contacts. - contacts = Contact.select() - - template = Template(''' -

All Contacts

- - #for $contact in $contacts -
$contact.lastName, $contact.firstName - [Edit] - [Delete] -
- #end for - -

[Add new contact]

- ''', [locals(), globals()]) - - return template.respond() - - index.exposed = True - - - def edit(self, id = 0): - # we really want id as an integer. Since GET/POST parameters - # are always passed as strings, let's convert it. - id = int(id) - - if id > 0: - # if an id is specified, we're editing an existing contact. - contact = Contact.get(id) - title = "Edit Contact" - else: - # if no id is specified, we're entering a new contact. - contact = None - title = "New Contact" - - - # In the following template code, please note that we use - # Cheetah's $getVar() construct for the form values. We have - # to do this because contact may be set to None (see above). - template = Template(''' -

$title

- - - - Last Name:
- First Name:
- Phone:
- Email:
- URL:
- -
- ''', [locals(), globals()]) - - return template.respond() - - edit.exposed = True - - - def delete(self, id): - # Delete the specified contact - contact = Contact.get(int(id)) - contact.destroySelf() - return 'Deleted. Return to Index' - - delete.exposed = True - - - def store(self, lastName, firstName, phone, email, url, id = None): - if id and int(id) > 0: - # If an id was specified, update an existing contact. - contact = Contact.get(int(id)) - - # We could set one field after another, but that would - # cause multiple UPDATE clauses. So we'll just do it all - # in a single pass through the set() method. - contact.set( - lastName = lastName, - firstName = firstName, - phone = phone, - email = email, - url = url) - else: - # Otherwise, add a new contact. - contact = Contact( - lastName = lastName, - firstName = firstName, - phone = phone, - email = email, - url = url) - - return 'Stored. Return to Index' - - store.exposed = True - - - def reset(self): - # Drop existing table - Contact.dropTable(True) - - # Create new table - Contact.createTable() - - # Create some sample data - Contact( - firstName = 'Hendrik', - lastName = 'Mans', - email = 'hendrik@mans.de', - phone = '++49 89 12345678', - url = 'http://www.mornography.de') - - return "reset completed!" - - reset.exposed = True - - -print("If you're running this application for the first time, please go to http://localhost:8080/reset once in order to create the database!") - -cherrypy.quickstart(ContactManager()) diff --git a/libs/cherrypy/tutorial/custom_error.html b/libs/cherrypy/tutorial/custom_error.html deleted file mode 100644 index d0f30c8..0000000 --- a/libs/cherrypy/tutorial/custom_error.html +++ /dev/null @@ -1,14 +0,0 @@ - - - - - 403 Unauthorized - - -

You can't do that!

-

%(message)s

-

This is a custom error page that is read from a file.

-

%(traceback)s
- - diff --git a/libs/cherrypy/tutorial/pdf_file.pdf b/libs/cherrypy/tutorial/pdf_file.pdf deleted file mode 100644 index 38b4f15eabdd65d4a674cb32034361245aa7b97e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 85698 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 diff --git a/libs/cherrypy/tutorial/tut01_helloworld.py b/libs/cherrypy/tutorial/tut01_helloworld.py deleted file mode 100644 index ef94760..0000000 --- a/libs/cherrypy/tutorial/tut01_helloworld.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Tutorial - Hello World - -The most basic (working) CherryPy application possible. -""" - -# Import CherryPy global namespace -import cherrypy - -class HelloWorld: - """ Sample request handler class. """ - - def index(self): - # CherryPy will call this method for the root URI ("/") and send - # its return value to the client. Because this is tutorial - # lesson number 01, we'll just send something really simple. - # How about... - return "Hello world!" - - # Expose the index method through the web. CherryPy will never - # publish methods that don't have the exposed attribute set to True. - index.exposed = True - - -import os.path -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(HelloWorld(), config=tutconf) -else: - # This branch is for the test suite; you can ignore it. - cherrypy.tree.mount(HelloWorld(), config=tutconf) diff --git a/libs/cherrypy/tutorial/tut02_expose_methods.py b/libs/cherrypy/tutorial/tut02_expose_methods.py deleted file mode 100644 index 600fca3..0000000 --- a/libs/cherrypy/tutorial/tut02_expose_methods.py +++ /dev/null @@ -1,32 +0,0 @@ -""" -Tutorial - Multiple methods - -This tutorial shows you how to link to other methods of your request -handler. -""" - -import cherrypy - -class HelloWorld: - - def index(self): - # Let's link to another method here. - return 'We have an important message for you!' - index.exposed = True - - def showMessage(self): - # Here's the important message! - return "Hello world!" - showMessage.exposed = True - -import os.path -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(HelloWorld(), config=tutconf) -else: - # This branch is for the test suite; you can ignore it. - cherrypy.tree.mount(HelloWorld(), config=tutconf) diff --git a/libs/cherrypy/tutorial/tut03_get_and_post.py b/libs/cherrypy/tutorial/tut03_get_and_post.py deleted file mode 100644 index 283477d..0000000 --- a/libs/cherrypy/tutorial/tut03_get_and_post.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -Tutorial - Passing variables - -This tutorial shows you how to pass GET/POST variables to methods. -""" - -import cherrypy - - -class WelcomePage: - - def index(self): - # Ask for the user's name. - return ''' -
- What is your name? - - -
''' - index.exposed = True - - def greetUser(self, name = None): - # CherryPy passes all GET and POST variables as method parameters. - # It doesn't make a difference where the variables come from, how - # large their contents are, and so on. - # - # You can define default parameter values as usual. In this - # example, the "name" parameter defaults to None so we can check - # if a name was actually specified. - - if name: - # Greet the user! - return "Hey %s, what's up?" % name - else: - if name is None: - # No name was specified - return 'Please enter your name here.' - else: - return 'No, really, enter your name here.' - greetUser.exposed = True - - -import os.path -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(WelcomePage(), config=tutconf) -else: - # This branch is for the test suite; you can ignore it. - cherrypy.tree.mount(WelcomePage(), config=tutconf) diff --git a/libs/cherrypy/tutorial/tut04_complex_site.py b/libs/cherrypy/tutorial/tut04_complex_site.py deleted file mode 100644 index b4d820e..0000000 --- a/libs/cherrypy/tutorial/tut04_complex_site.py +++ /dev/null @@ -1,98 +0,0 @@ -""" -Tutorial - Multiple objects - -This tutorial shows you how to create a site structure through multiple -possibly nested request handler objects. -""" - -import cherrypy - - -class HomePage: - def index(self): - return ''' -

Hi, this is the home page! Check out the other - fun stuff on this site:

- - ''' - index.exposed = True - - -class JokePage: - def index(self): - return ''' -

"In Python, how do you create a string of random - characters?" -- "Read a Perl file!"

-

[Return]

''' - index.exposed = True - - -class LinksPage: - def __init__(self): - # Request handler objects can create their own nested request - # handler objects. Simply create them inside their __init__ - # methods! - self.extra = ExtraLinksPage() - - def index(self): - # Note the way we link to the extra links page (and back). - # As you can see, this object doesn't really care about its - # absolute position in the site tree, since we use relative - # links exclusively. - return ''' -

Here are some useful links:

- - - -

You can check out some extra useful - links here.

- -

[Return]

- ''' - index.exposed = True - - -class ExtraLinksPage: - def index(self): - # Note the relative link back to the Links page! - return ''' -

Here are some extra useful links:

- - - -

[Return to links page]

''' - index.exposed = True - - -# Of course we can also mount request handler objects right here! -root = HomePage() -root.joke = JokePage() -root.links = LinksPage() - -# Remember, we don't need to mount ExtraLinksPage here, because -# LinksPage does that itself on initialization. In fact, there is -# no reason why you shouldn't let your root object take care of -# creating all contained request handler objects. - - -import os.path -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(root, config=tutconf) -else: - # This branch is for the test suite; you can ignore it. - cherrypy.tree.mount(root, config=tutconf) - diff --git a/libs/cherrypy/tutorial/tut05_derived_objects.py b/libs/cherrypy/tutorial/tut05_derived_objects.py deleted file mode 100644 index 3d4ec9b..0000000 --- a/libs/cherrypy/tutorial/tut05_derived_objects.py +++ /dev/null @@ -1,83 +0,0 @@ -""" -Tutorial - Object inheritance - -You are free to derive your request handler classes from any base -class you wish. In most real-world applications, you will probably -want to create a central base class used for all your pages, which takes -care of things like printing a common page header and footer. -""" - -import cherrypy - - -class Page: - # Store the page title in a class attribute - title = 'Untitled Page' - - def header(self): - return ''' - - - %s - - -

%s

- ''' % (self.title, self.title) - - def footer(self): - return ''' - - - ''' - - # Note that header and footer don't get their exposed attributes - # set to True. This isn't necessary since the user isn't supposed - # to call header or footer directly; instead, we'll call them from - # within the actually exposed handler methods defined in this - # class' subclasses. - - -class HomePage(Page): - # Different title for this page - title = 'Tutorial 5' - - def __init__(self): - # create a subpage - self.another = AnotherPage() - - def index(self): - # Note that we call the header and footer methods inherited - # from the Page class! - return self.header() + ''' -

- Isn't this exciting? There's - another page, too! -

- ''' + self.footer() - index.exposed = True - - -class AnotherPage(Page): - title = 'Another Page' - - def index(self): - return self.header() + ''' -

- And this is the amazing second page! -

- ''' + self.footer() - index.exposed = True - - -import os.path -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(HomePage(), config=tutconf) -else: - # This branch is for the test suite; you can ignore it. - cherrypy.tree.mount(HomePage(), config=tutconf) - diff --git a/libs/cherrypy/tutorial/tut06_default_method.py b/libs/cherrypy/tutorial/tut06_default_method.py deleted file mode 100644 index fe24f38..0000000 --- a/libs/cherrypy/tutorial/tut06_default_method.py +++ /dev/null @@ -1,64 +0,0 @@ -""" -Tutorial - The default method - -Request handler objects can implement a method called "default" that -is called when no other suitable method/object could be found. -Essentially, if CherryPy2 can't find a matching request handler object -for the given request URI, it will use the default method of the object -located deepest on the URI path. - -Using this mechanism you can easily simulate virtual URI structures -by parsing the extra URI string, which you can access through -cherrypy.request.virtualPath. - -The application in this tutorial simulates an URI structure looking -like /users/. Since the bit will not be found (as -there are no matching methods), it is handled by the default method. -""" - -import cherrypy - - -class UsersPage: - - def index(self): - # Since this is just a stupid little example, we'll simply - # display a list of links to random, made-up users. In a real - # application, this could be generated from a database result set. - return ''' - Remi Delon
- Hendrik Mans
- Lorenzo Lamas
- ''' - index.exposed = True - - def default(self, user): - # Here we react depending on the virtualPath -- the part of the - # path that could not be mapped to an object method. In a real - # application, we would probably do some database lookups here - # instead of the silly if/elif/else construct. - if user == 'remi': - out = "Remi Delon, CherryPy lead developer" - elif user == 'hendrik': - out = "Hendrik Mans, CherryPy co-developer & crazy German" - elif user == 'lorenzo': - out = "Lorenzo Lamas, famous actor and singer!" - else: - out = "Unknown user. :-(" - - return '%s (back)' % out - default.exposed = True - - -import os.path -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(UsersPage(), config=tutconf) -else: - # This branch is for the test suite; you can ignore it. - cherrypy.tree.mount(UsersPage(), config=tutconf) - diff --git a/libs/cherrypy/tutorial/tut07_sessions.py b/libs/cherrypy/tutorial/tut07_sessions.py deleted file mode 100644 index 4b1386b..0000000 --- a/libs/cherrypy/tutorial/tut07_sessions.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -Tutorial - Sessions - -Storing session data in CherryPy applications is very easy: cherrypy -provides a dictionary called "session" that represents the session -data for the current user. If you use RAM based sessions, you can store -any kind of object into that dictionary; otherwise, you are limited to -objects that can be pickled. -""" - -import cherrypy - - -class HitCounter: - - _cp_config = {'tools.sessions.on': True} - - def index(self): - # Increase the silly hit counter - count = cherrypy.session.get('count', 0) + 1 - - # Store the new value in the session dictionary - cherrypy.session['count'] = count - - # And display a silly hit count message! - return ''' - During your current session, you've viewed this - page %s times! Your life is a patio of fun! - ''' % count - index.exposed = True - - -import os.path -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(HitCounter(), config=tutconf) -else: - # This branch is for the test suite; you can ignore it. - cherrypy.tree.mount(HitCounter(), config=tutconf) - diff --git a/libs/cherrypy/tutorial/tut08_generators_and_yield.py b/libs/cherrypy/tutorial/tut08_generators_and_yield.py deleted file mode 100644 index a6fbdc2..0000000 --- a/libs/cherrypy/tutorial/tut08_generators_and_yield.py +++ /dev/null @@ -1,47 +0,0 @@ -""" -Bonus Tutorial: Using generators to return result bodies - -Instead of returning a complete result string, you can use the yield -statement to return one result part after another. This may be convenient -in situations where using a template package like CherryPy or Cheetah -would be overkill, and messy string concatenation too uncool. ;-) -""" - -import cherrypy - - -class GeneratorDemo: - - def header(self): - return "

Generators rule!

" - - def footer(self): - return "" - - def index(self): - # Let's make up a list of users for presentation purposes - users = ['Remi', 'Carlos', 'Hendrik', 'Lorenzo Lamas'] - - # Every yield line adds one part to the total result body. - yield self.header() - yield "

List of users:

" - - for user in users: - yield "%s
" % user - - yield self.footer() - index.exposed = True - - -import os.path -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(GeneratorDemo(), config=tutconf) -else: - # This branch is for the test suite; you can ignore it. - cherrypy.tree.mount(GeneratorDemo(), config=tutconf) - diff --git a/libs/cherrypy/tutorial/tut09_files.py b/libs/cherrypy/tutorial/tut09_files.py deleted file mode 100644 index 4c8e581..0000000 --- a/libs/cherrypy/tutorial/tut09_files.py +++ /dev/null @@ -1,107 +0,0 @@ -""" - -Tutorial: File upload and download - -Uploads -------- - -When a client uploads a file to a CherryPy application, it's placed -on disk immediately. CherryPy will pass it to your exposed method -as an argument (see "myFile" below); that arg will have a "file" -attribute, which is a handle to the temporary uploaded file. -If you wish to permanently save the file, you need to read() -from myFile.file and write() somewhere else. - -Note the use of 'enctype="multipart/form-data"' and 'input type="file"' -in the HTML which the client uses to upload the file. - - -Downloads ---------- - -If you wish to send a file to the client, you have two options: -First, you can simply return a file-like object from your page handler. -CherryPy will read the file and serve it as the content (HTTP body) -of the response. However, that doesn't tell the client that -the response is a file to be saved, rather than displayed. -Use cherrypy.lib.static.serve_file for that; it takes four -arguments: - -serve_file(path, content_type=None, disposition=None, name=None) - -Set "name" to the filename that you expect clients to use when they save -your file. Note that the "name" argument is ignored if you don't also -provide a "disposition" (usually "attachement"). You can manually set -"content_type", but be aware that if you also use the encoding tool, it -may choke if the file extension is not recognized as belonging to a known -Content-Type. Setting the content_type to "application/x-download" works -in most cases, and should prompt the user with an Open/Save dialog in -popular browsers. - -""" - -import os -localDir = os.path.dirname(__file__) -absDir = os.path.join(os.getcwd(), localDir) - -import cherrypy -from cherrypy.lib import static - - -class FileDemo(object): - - def index(self): - return """ - -

Upload a file

-
- filename:
- -
-

Download a file

- This one - - """ - index.exposed = True - - def upload(self, myFile): - out = """ - - myFile length: %s
- myFile filename: %s
- myFile mime-type: %s - - """ - - # Although this just counts the file length, it demonstrates - # how to read large files in chunks instead of all at once. - # CherryPy reads the uploaded file into a temporary file; - # myFile.file.read reads from that. - size = 0 - while True: - data = myFile.file.read(8192) - if not data: - break - size += len(data) - - return out % (size, myFile.filename, myFile.content_type) - upload.exposed = True - - def download(self): - path = os.path.join(absDir, "pdf_file.pdf") - return static.serve_file(path, "application/x-download", - "attachment", os.path.basename(path)) - download.exposed = True - - -import os.path -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(FileDemo(), config=tutconf) -else: - # This branch is for the test suite; you can ignore it. - cherrypy.tree.mount(FileDemo(), config=tutconf) diff --git a/libs/cherrypy/tutorial/tut10_http_errors.py b/libs/cherrypy/tutorial/tut10_http_errors.py deleted file mode 100644 index dfa5733..0000000 --- a/libs/cherrypy/tutorial/tut10_http_errors.py +++ /dev/null @@ -1,81 +0,0 @@ -""" - -Tutorial: HTTP errors - -HTTPError is used to return an error response to the client. -CherryPy has lots of options regarding how such errors are -logged, displayed, and formatted. - -""" - -import os -localDir = os.path.dirname(__file__) -curpath = os.path.normpath(os.path.join(os.getcwd(), localDir)) - -import cherrypy - - -class HTTPErrorDemo(object): - - # Set a custom response for 403 errors. - _cp_config = {'error_page.403' : os.path.join(curpath, "custom_error.html")} - - def index(self): - # display some links that will result in errors - tracebacks = cherrypy.request.show_tracebacks - if tracebacks: - trace = 'off' - else: - trace = 'on' - - return """ - -

Toggle tracebacks %s

-

Click me; I'm a broken link!

-

Use a custom error page from a file.

-

These errors are explicitly raised by the application:

- -

You can also set the response body - when you raise an error.

- - """ % trace - index.exposed = True - - def toggleTracebacks(self): - # simple function to toggle tracebacks on and off - tracebacks = cherrypy.request.show_tracebacks - cherrypy.config.update({'request.show_tracebacks': not tracebacks}) - - # redirect back to the index - raise cherrypy.HTTPRedirect('/') - toggleTracebacks.exposed = True - - def error(self, code): - # raise an error based on the get query - raise cherrypy.HTTPError(status = code) - error.exposed = True - - def messageArg(self): - message = ("If you construct an HTTPError with a 'message' " - "argument, it wil be placed on the error page " - "(underneath the status line by default).") - raise cherrypy.HTTPError(500, message=message) - messageArg.exposed = True - - -import os.path -tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf') - -if __name__ == '__main__': - # CherryPy always starts with app.root when trying to map request URIs - # to objects, so we need to mount a request handler root. A request - # to '/' will be mapped to HelloWorld().index(). - cherrypy.quickstart(HTTPErrorDemo(), config=tutconf) -else: - # This branch is for the test suite; you can ignore it. - cherrypy.tree.mount(HTTPErrorDemo(), config=tutconf) diff --git a/libs/cherrypy/tutorial/tutorial.conf b/libs/cherrypy/tutorial/tutorial.conf deleted file mode 100644 index 6537fd3..0000000 --- a/libs/cherrypy/tutorial/tutorial.conf +++ /dev/null @@ -1,4 +0,0 @@ -[global] -server.socket_host = "127.0.0.1" -server.socket_port = 8080 -server.thread_pool = 10 diff --git a/settings.py b/settings.py index 85a1c71..00e1963 100644 --- a/settings.py +++ b/settings.py @@ -1,7 +1,18 @@ # -*- coding: utf-8 -*- import os - -DEBUG = True ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -STATIC_FILES_ROOT = os.path.join(ROOT_DIR, 'static') \ No newline at end of file +STATIC_FILES_ROOT = os.path.join(ROOT_DIR, 'static') + +##################################################### +# You can start editing settings after this comment # +##################################################### + +# debug will get you error message and auto reload +# don't set this to True in production +DEBUG = True + +# absolute path where the paste files should be store +# default in projectdirectory/static/content/ +# use "/" even under Windows +PASTE_FILES_ROOT = os.path.join(STATIC_FILES_ROOT, 'content') \ No newline at end of file diff --git a/src/paste.py b/src/paste.py index 1ab0090..69dc625 100644 --- a/src/paste.py +++ b/src/paste.py @@ -26,18 +26,21 @@ class Paste(object): def __init__(self, uuid=None, content=None, - expiration=u'burn_after_reading', + expiration=None, comments=None): self.content = content + self.expiration = expiration + self.comments = comments + if isinstance(self.content, unicode): self.content = self.content.encode('utf8') + if (not isinstance(expiration, datetime) and + 'burn_after_reading' not in str(expiration)): + self.expiration = self.get_expiration(self.expiration) + self.uuid = uuid or hashlib.sha1(self.content).hexdigest() - self.expiration = self.get_expiration(expiration) - self.comments = comments - - def get_expiration(self, expiration): @@ -57,7 +60,7 @@ class Paste(object): Generic static content path builder. Return a path to a location in the static content file dir. """ - return os.path.join(settings.STATIC_FILES_ROOT, u'content', *dirs) + return os.path.join(settings.PASTE_FILES_ROOT, *dirs) @classmethod @@ -88,13 +91,14 @@ class Paste(object): expiration = paste.next().strip() content = paste.next().strip() comments = paste.read()[:-1] # remove the last coma - if expiration != u'burn_after_reading': + if "burn_after_reading" not in str(expiration): expiration = datetime.strptime(expiration,'%Y-%m-%d %H:%M:%S.%f') except StopIteration: raise TypeError(u'File %s is malformed' % path) except (IOError, OSError): raise ValueError(u'Can not open paste from file %s' % path) + return Paste(uuid=uuid, comments=comments, expiration=expiration, content=content) @@ -115,7 +119,6 @@ class Paste(object): If comments are passed, they are expected to be serialized already. """ - data = {'content': self.content} head, tail = self.uuid[:2], self.uuid[2:4] # the static files are saved in project_dir/static/xx/yy/uuid @@ -139,6 +142,13 @@ class Paste(object): if not os.path.isdir(path): os.mkdir(path) + # add a timestamp to burn after reading to allow + # a quick period of time where you can redirect to the page without + # deleting the paste + if self.expiration == "burn_after_reading": + self.expiration = self.expiration + '#%s' % datetime.now() + + # writethe paste with open(self.path, 'w') as f: f.write(unicode(self.expiration) + '\n') f.write(self.content + '\n') @@ -152,7 +162,7 @@ class Paste(object): """ Delete the paste file. """ - os.path.remove(self.path) + os.remove(self.path) @classmethod diff --git a/start.py b/start.py index cd806c7..21c738c 100644 --- a/start.py +++ b/start.py @@ -8,6 +8,8 @@ import os import hashlib +from datetime import datetime, timedelta + from src import settings, setup_path, Paste setup_path() @@ -31,28 +33,52 @@ def create_paste(): try: content = unicode(request.forms.get('content', ''), 'utf8') except UnicodeDecodeError: - content = u'' + return {'status': 'error', + 'message': u"Encoding error: the paste couldn't be saved."} if content: expiration = request.forms.get('expiration', u'burn_after_reading') paste = Paste(expiration=expiration, content=content) paste.save() + return {'status': 'ok', + 'paste': paste.uuid} - return paste.uuid - - return '' + return {'status': 'error', + 'message': u"Serveur error: the paste couldn't be saved. Please try later."} @app.route('/paste/:paste_id') @view('paste') def display_paste(paste_id): + + now = datetime.now() + keep_alive = False try: paste = Paste.load(paste_id) - except (TypeError, ValueError): - abort(404, u"This paste does't exist or has expired") + # Delete the paste if it expired: + if 'burn_after_reading' in str(paste.expiration): + # burn_after_reading contains the paste creation date + # if this read appends 10 seconds after the creation date + # we don't delete the paste because it means it's the redirection + # to the paste that happens during the paste creation + try: + keep_alive = paste.expiration.split('#')[1] + keep_alive = datetime.strptime(keep_alive,'%Y-%m-%d %H:%M:%S.%f') + keep_alive = now < keep_alive + timedelta(seconds=10) + except IndexError: + keep_alive = False + if not keep_alive: + paste.delete() - return {'paste': paste} + elif paste.expiration < now: + paste.delete() + raise ValueError() + + except (TypeError, ValueError): + abort(404, u"This paste doesn't exist or has expired") + + return {'paste': paste, 'keep_alive': keep_alive} @app.route('/static/') diff --git a/static/js/behavior.js b/static/js/behavior.js index 3b32cbe..8c4b528 100644 --- a/static/js/behavior.js +++ b/static/js/behavior.js @@ -28,11 +28,11 @@ $('button[type=submit]').click(function(e){ var data = {content: zerobin.encrypt(key, paste), expiration: expiration} $.post('/paste/create', data) - .error(function() { + .error(function(error) { alert('Paste could not be saved. Please try again later.'); }) .success(function(data) { - window.location = '/paste/' + data + '#' + key; + window.location = ('/paste/' + data['paste'] + '#' + key); }); } diff --git a/views/paste.tpl b/views/paste.tpl index cb7b441..39eed02 100644 --- a/views/paste.tpl +++ b/views/paste.tpl @@ -1,3 +1,15 @@ +%if "burn_after_reading" in str(paste.expiration): +
+ Warning! + %if keep_alive: + This paste will be deleted the next time it is read. + %else: + This paste has self-destructed. If you close this windows, there is not way + to recover it. + %end +
+%end +