├── cherrypy ├── favicon.ico ├── lib │ ├── http.py │ ├── __init__.py │ ├── xmlrpcutil.py │ ├── auth.py │ ├── auth_basic.py │ ├── jsontools.py │ ├── profiler.py │ ├── gctools.py │ ├── covercp.py │ ├── httpauth.py │ └── auth_digest.py ├── process │ ├── __init__.py │ └── win32.py ├── wsgiserver │ ├── __init__.py │ ├── ssl_builtin.py │ └── ssl_pyopenssl.py ├── LICENSE.txt ├── _cpwsgi_server.py ├── cherryd ├── _cpnative_server.py ├── _cpthreadinglocal.py ├── _cpserver.py ├── _cpconfig.py ├── _cpcompat.py ├── _cptree.py ├── _cpmodpy.py └── _cpchecker.py ├── static ├── imgs │ ├── love.png │ ├── favicon.ico │ └── pandora.png ├── css │ ├── styles.css │ └── mobile.css ├── html │ ├── tv.html │ ├── index.html │ └── mobile.html └── js │ └── pidora.js ├── .gitignore ├── cpy.conf ├── README.md ├── hello.py ├── template.py ├── bar-update.py ├── install.sh └── pidora.py /cherrypy/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacroe/pidora/HEAD/cherrypy/favicon.ico -------------------------------------------------------------------------------- /static/imgs/love.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacroe/pidora/HEAD/static/imgs/love.png -------------------------------------------------------------------------------- /static/imgs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacroe/pidora/HEAD/static/imgs/favicon.ico -------------------------------------------------------------------------------- /static/imgs/pandora.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jacroe/pidora/HEAD/static/imgs/pandora.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Files to ignore because of end-use 2 | ctl 3 | curSong.json 4 | msg 5 | stationList 6 | lastNews 7 | *.pyc 8 | 9 | # Files to ignore because of development 10 | pidora.sublime-project 11 | pidora.sublime-workspace 12 | -------------------------------------------------------------------------------- /cherrypy/lib/http.py: -------------------------------------------------------------------------------- 1 | import warnings 2 | warnings.warn('cherrypy.lib.http has been deprecated and will be removed ' 3 | 'in CherryPy 3.3 use cherrypy.lib.httputil instead.', 4 | DeprecationWarning) 5 | 6 | from cherrypy.lib.httputil import * 7 | 8 | -------------------------------------------------------------------------------- /cpy.conf: -------------------------------------------------------------------------------- 1 | [global] 2 | server.socket_host = "0.0.0.0" 3 | server.socket_port = 8080 4 | 5 | [/] 6 | tools.staticdir.root = "/home/pi/pidora" 7 | 8 | [/css] 9 | tools.staticdir.on = True 10 | tools.staticdir.dir = "static/css" 11 | 12 | [/js] 13 | tools.staticdir.on = True 14 | tools.staticdir.dir = "static/js" 15 | 16 | [/imgs] 17 | tools.staticdir.on = True 18 | tools.staticdir.dir = "static/imgs" 19 | -------------------------------------------------------------------------------- /cherrypy/process/__init__.py: -------------------------------------------------------------------------------- 1 | """Site container for an HTTP server. 2 | 3 | A Web Site Process Bus object is used to connect applications, servers, 4 | and frameworks with site-wide services such as daemonization, process 5 | reload, signal handling, drop privileges, PID file management, logging 6 | for all of these, and many more. 7 | 8 | The 'plugins' module defines a few abstract and concrete services for 9 | use with the bus. Some use tool-specific channels; see the documentation 10 | for each class. 11 | """ 12 | 13 | from cherrypy.process.wspbus import bus 14 | from cherrypy.process import plugins, servers 15 | -------------------------------------------------------------------------------- /cherrypy/wsgiserver/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ['HTTPRequest', 'HTTPConnection', 'HTTPServer', 2 | 'SizeCheckWrapper', 'KnownLengthRFile', 'ChunkedRFile', 3 | 'MaxSizeExceeded', 'NoSSLError', 'FatalSSLAlert', 4 | 'WorkerThread', 'ThreadPool', 'SSLAdapter', 5 | 'CherryPyWSGIServer', 6 | 'Gateway', 'WSGIGateway', 'WSGIGateway_10', 'WSGIGateway_u0', 7 | 'WSGIPathInfoDispatcher', 'get_ssl_adapter_class'] 8 | 9 | import sys 10 | if sys.version_info < (3, 0): 11 | from wsgiserver2 import * 12 | else: 13 | # Le sigh. Boo for backward-incompatible syntax. 14 | exec('from .wsgiserver3 import *') 15 | -------------------------------------------------------------------------------- /static/css/styles.css: -------------------------------------------------------------------------------- 1 | body {background-color: #000; color: #fff; font-family: Arial, sans-serif;} 2 | 3 | #content, #stationList, #newStation, #msg {width: 80%; position:absolute; top:50%; left:50%; height:330px; width:1000px; margin-top:-165px; margin-left:-500px; font-weight: bold;} 4 | #content h1 {font-size:4em;} 5 | #msg h1 {font-size:6em; text-align:center;} 6 | #content h2 {font-size:2em; margin:0} 7 | #content h2.album {font-style: italic;} 8 | #content img.albumart {max-width:300px; height:auto; float:left; padding-right:10px; margin-bottom:100px} 9 | #content img.love {float:right; width: 20px;} 10 | #content p.details {display:none;} 11 | 12 | #controls {float:right; text-align: right;} 13 | a:hover {cursor: pointer;} 14 | -------------------------------------------------------------------------------- /static/html/tv.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pidora 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 21 | 22 | 24 | 25 | 29 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /cherrypy/lib/__init__.py: -------------------------------------------------------------------------------- 1 | """CherryPy Library""" 2 | 3 | # Deprecated in CherryPy 3.2 -- remove in CherryPy 3.3 4 | from cherrypy.lib.reprconf import unrepr, modules, attributes 5 | 6 | class file_generator(object): 7 | """Yield the given input (a file object) in chunks (default 64k). (Core)""" 8 | 9 | def __init__(self, input, chunkSize=65536): 10 | self.input = input 11 | self.chunkSize = chunkSize 12 | 13 | def __iter__(self): 14 | return self 15 | 16 | def __next__(self): 17 | chunk = self.input.read(self.chunkSize) 18 | if chunk: 19 | return chunk 20 | else: 21 | if hasattr(self.input, 'close'): 22 | self.input.close() 23 | raise StopIteration() 24 | next = __next__ 25 | 26 | def file_generator_limited(fileobj, count, chunk_size=65536): 27 | """Yield the given file object in chunks, stopping after `count` 28 | bytes has been emitted. Default chunk size is 64kB. (Core) 29 | """ 30 | remaining = count 31 | while remaining > 0: 32 | chunk = fileobj.read(min(chunk_size, remaining)) 33 | chunklen = len(chunk) 34 | if chunklen == 0: 35 | return 36 | remaining -= chunklen 37 | yield chunk 38 | 39 | def set_vary_header(response, header_name): 40 | "Add a Vary header to a response" 41 | varies = response.headers.get("Vary", "") 42 | varies = [x.strip() for x in varies.split(",") if x.strip()] 43 | if header_name not in varies: 44 | varies.append(header_name) 45 | response.headers['Vary'] = ", ".join(varies) 46 | -------------------------------------------------------------------------------- /static/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pidora 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | Pause 15 | Next 16 | Love 17 | Ban 18 | Tired 19 | Explain 20 | Station 21 | V+ 22 | V- 23 | 24 |
25 | 26 | 34 | 35 | 37 | 38 | 42 | 43 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /cherrypy/LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2004-2011, CherryPy Team (team@cherrypy.org) 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | * Neither the name of the CherryPy Team nor the names of its contributors 13 | may be used to endorse or promote products derived from this software 14 | without specific prior written permission. 15 | 16 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 17 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 18 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 19 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 20 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 21 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 22 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 23 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 24 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 25 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Pidora 2 | ====== 3 | 4 | [![Clone in Koding](http://kbutton.org/clone.png?v1378906380)](http://kbutton.org/jacroe/pidora) 5 | 6 | Installation 7 | ------------ 8 | 9 | 1. Run the following command `wget https://raw.github.com/jacroe/pidora/master/install.sh && chmod +x install.sh && ./install.sh`. 10 | 2. Follow the instructions [here](http://raspberrypi.stackexchange.com/questions/752/how-do-i-prevent-the-screen-from-going-blank) to make the monitor not turn itself off. However, save it to the `~/.xsessionrc` instead of `~/.xinitrc`. 11 | 3. Finally, remove all the icons on the desktop and set the statusbar to hide automatically. This isn't necessary but it gives less an impression that this is a computer. 12 | 13 | Updates 14 | ------- 15 | 16 | Updates are easy thinks to Git. Simply `cd ~/pidora` and then `git pull`. If Pidora is currently running, you'll need to kill pianobar and restart the server. This can easily be done by simply restarting the Pi. Please note, updates may contain new bugs. Use at your own risk. If your installation does become unusuable, may I suggest our [releases](https://github.com/jacroe/pidora/releases)? 17 | 18 | 19 | Contact me 20 | ========== 21 | 22 | You can shoot me an email or submit an issue at [GitHub](https://github.com/jacroe/pidora/issues/new) if you have a question/problem or a suggestion. I welcome them with open arms. 23 | 24 | If you found this useful, I also welcome tips with open arms! You can tip me via [Gittip](http://gittip.com/jacroe), [Paypal](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=XC7VG35XEHN8W), or [Bitcoin](http://jacroe.com/bitcoin.html). I'll use these to pay for bills and/or Mountain Dew and pizza. Thank you, and best wishes! 25 | -------------------------------------------------------------------------------- /cherrypy/lib/xmlrpcutil.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | import cherrypy 4 | from cherrypy._cpcompat import ntob 5 | 6 | def get_xmlrpclib(): 7 | try: 8 | import xmlrpc.client as x 9 | except ImportError: 10 | import xmlrpclib as x 11 | return x 12 | 13 | def process_body(): 14 | """Return (params, method) from request body.""" 15 | try: 16 | return get_xmlrpclib().loads(cherrypy.request.body.read()) 17 | except Exception: 18 | return ('ERROR PARAMS', ), 'ERRORMETHOD' 19 | 20 | 21 | def patched_path(path): 22 | """Return 'path', doctored for RPC.""" 23 | if not path.endswith('/'): 24 | path += '/' 25 | if path.startswith('/RPC2/'): 26 | # strip the first /rpc2 27 | path = path[5:] 28 | return path 29 | 30 | 31 | def _set_response(body): 32 | # The XML-RPC spec (http://www.xmlrpc.com/spec) says: 33 | # "Unless there's a lower-level error, always return 200 OK." 34 | # Since Python's xmlrpclib interprets a non-200 response 35 | # as a "Protocol Error", we'll just return 200 every time. 36 | response = cherrypy.response 37 | response.status = '200 OK' 38 | response.body = ntob(body, 'utf-8') 39 | response.headers['Content-Type'] = 'text/xml' 40 | response.headers['Content-Length'] = len(body) 41 | 42 | 43 | def respond(body, encoding='utf-8', allow_none=0): 44 | xmlrpclib = get_xmlrpclib() 45 | if not isinstance(body, xmlrpclib.Fault): 46 | body = (body,) 47 | _set_response(xmlrpclib.dumps(body, methodresponse=1, 48 | encoding=encoding, 49 | allow_none=allow_none)) 50 | 51 | def on_error(*args, **kwargs): 52 | body = str(sys.exc_info()[1]) 53 | xmlrpclib = get_xmlrpclib() 54 | _set_response(xmlrpclib.dumps(xmlrpclib.Fault(1, body))) 55 | 56 | -------------------------------------------------------------------------------- /hello.py: -------------------------------------------------------------------------------- 1 | import cherrypy, os, json as libjson, pidora, template 2 | current_dir = os.path.dirname(os.path.abspath(__file__)) + "/" 3 | cherrypy.engine.autoreload.unsubscribe() 4 | class Pidora(): 5 | 6 | data = dict(pianobar=None, songData=None) 7 | 8 | #Comment the line below to cancel autostart 9 | data['pianobar'] = pidora.process(['pianobar'], True) 10 | 11 | @cherrypy.expose 12 | def index(self): 13 | songData = pidora.getSongData(self.data) 14 | return template.index(songData) 15 | 16 | @cherrypy.expose 17 | def api(self, json=None): 18 | cherrypy.response.headers['Content-Type'] = 'application/json' 19 | reply = pidora.api(self.data, json) 20 | return reply["json"] 21 | 22 | @cherrypy.expose 23 | def start(self): 24 | if self.data['pianobar'] is None: 25 | pidora.api(self.data, '{"method":"Pianobar.Start", "id":1}') 26 | return "Pianobar has started

Pianobar is starting

" 27 | else: 28 | return "Pianobar is already running

Pianobar is already running

" 29 | 30 | @cherrypy.expose 31 | def quit(self): 32 | if self.data['pianobar']: 33 | pidora.api(self.data, '{"method":"Pianobar.Quit", "id":1}') 34 | return "Pianobar has quit

Pianobar has quit

" 35 | else: 36 | return "Pianobar is not running

Pianobar is not running

" 37 | 38 | @cherrypy.expose 39 | def mobile(self, c=None): 40 | if c is None: 41 | songData = pidora.getSongData(self.data) 42 | return template.mobile(songData) 43 | else: 44 | json = libjson.dumps(dict(method="Control", id=0, command=c)) 45 | pidora.api(self.data, json) 46 | raise cherrypy.HTTPRedirect('/mobile') 47 | m = mobile 48 | 49 | @cherrypy.expose 50 | def tv(self): 51 | songData = pidora.getSongData(self.data) 52 | return template.tv(songData) 53 | 54 | cherrypy.quickstart(Pidora(), config=current_dir + "cpy.conf") -------------------------------------------------------------------------------- /template.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | current_dir = os.path.dirname(os.path.abspath(__file__)) + "/" 4 | 5 | def index(songData): 6 | return open(current_dir + "static/html/index.html").read() 7 | 8 | def tv(songData): 9 | return open(current_dir + "static/html/tv.html").read() 10 | 11 | def mobile(songData): 12 | returnData = open(current_dir + "static/html/mobile.html").read() 13 | if "startup" not in songData: 14 | if not songData["artURL"]: 15 | songData["artURL"] = "imgs/pandora.png" 16 | returnData = returnData.replace("{SONGTITLE}", songData["title"]) 17 | returnData = returnData.replace("{SONGARTIST}", songData["artist"]) 18 | returnData = returnData.replace("{SONGALBUM}", songData["album"]) 19 | returnData = returnData.replace("{SONGARTURL}", songData["artURL"]) 20 | returnData = returnData.replace("{ALBUMARTALT}", songData["title"] + " by " + songData["artist"]) 21 | if songData["loved"]: 22 | returnData = returnData.replace("{LOVED}", "<3") 23 | else: 24 | returnData = returnData.replace("{LOVED}", "") 25 | if songData["isSong"] is True: 26 | returnData = returnData.replace("{EXPLANATION}", "1") 27 | else: 28 | returnData = returnData.replace("{EXPLANATION}", "3") 29 | elif songData["startup"]: 30 | returnData = returnData.replace("{SONGTITLE}", "We're starting up") 31 | returnData = returnData.replace("{SONGARTIST}", "Sit Tight") 32 | returnData = returnData.replace("{SONGALBUM}", "And Hang On") 33 | returnData = returnData.replace("{SONGARTURL}", "imgs/pandora.png") 34 | returnData = returnData.replace("{ALBUMARTALT}", "Pandora logo") 35 | returnData = returnData.replace("{LOVED}", "") 36 | returnData = returnData.replace("{EXPLANATION}", "2") 37 | else: 38 | returnData = returnData.replace("{SONGTITLE}", "We're Shutdown") 39 | returnData = returnData.replace("{SONGARTIST}", "Just Chillin'") 40 | returnData = returnData.replace("{SONGALBUM}", "And Taking It Easy") 41 | returnData = returnData.replace("{SONGARTURL}", "imgs/pandora.png") 42 | returnData = returnData.replace("{ALBUMARTALT}", "Pandora logo") 43 | returnData = returnData.replace("{LOVED}", "") 44 | returnData = returnData.replace("{EXPLANATION}", "2") 45 | return returnData -------------------------------------------------------------------------------- /static/html/mobile.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pidora | Mobile 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 22 |
23 |
24 | 25 |
{SONGTITLE} {LOVED}
26 | {SONGARTIST}
27 | {SONGALBUM}
28 | {ALBUMARTALT} 29 | 30 |
31 |
32 | 35 | 84 | 85 | -------------------------------------------------------------------------------- /bar-update.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import sys, csv, subprocess, os, json, requests 4 | from time import gmtime, strftime 5 | 6 | def process(command, new = False): 7 | if new: 8 | with open(os.devnull, "w") as fnull: result = subprocess.Popen(command, stdout = fnull, stderr = fnull) 9 | else: 10 | with open(os.devnull, "w") as fnull: result = subprocess.call(command, stdout = fnull, stderr = fnull) 11 | def buildJSON(title, artist, album, artURL, loved, explainURL, songDuration = 0, songPlayed = 0): 12 | data = '{"title": ' + json.dumps(title) + ',"artist": ' + json.dumps(artist) + ',"album": ' + json.dumps(album) + ',"artURL": ' + json.dumps(artURL) + ',"loved": ' + str(bool(loved)).lower() + ',"explainURL": ' + json.dumps(explainURL) + ', "songDuration": ' + str(songDuration) + ', "songPlayed": ' + str(songPlayed) + '}' 13 | return json.loads(data) 14 | def sendRequest(url, method, songData): 15 | requests.post(url, params=dict(json=json.dumps(dict(method=method, id=1, songData=songData)))) 16 | www = os.path.dirname(os.path.abspath(__file__)) + "/" 17 | url = "http://localhost:8080/api" 18 | 19 | event = sys.argv[1] 20 | lines = sys.stdin.readlines() 21 | fields = dict([line.strip().split("=", 1) for line in lines]) 22 | 23 | artist = fields["artist"] 24 | title = fields["title"] 25 | album = fields["album"] 26 | coverArt = fields["coverArt"] 27 | rating = int(fields["rating"]) 28 | detailUrl = fields["detailUrl"].split('?dc')[0] 29 | songDuration = fields["songDuration"] 30 | songPlayed = fields["songPlayed"] 31 | 32 | if event == "songstart" or event == "songexplain": 33 | sendRequest(url, "SetSongInfo", buildJSON(title, artist, album, coverArt, rating, detailUrl)) 34 | elif event == "songfinish": 35 | import feedparser 36 | feed = feedparser.parse("http://www.npr.org/rss/podcast.php?id=500005") 37 | if not os.path.lexists(www + "lastNews"): open(www + "lastNews", "w").write("-1") 38 | lastNews = int(open(www + "lastNews", "r").read()) 39 | currNews = feed.entries[0].updated_parsed.tm_hour 40 | currHour = int(strftime("%H", gmtime())) 41 | currMin = int(strftime("%M", gmtime())) 42 | if currNews != lastNews and currNews == currHour: 43 | open(www + "lastNews", "w").write(str(feed.entries[0].updated_parsed.tm_hour)) 44 | sendRequest(url, "SetSongInfo", buildJSON(feed.entries[0].title, feed.feed.title, feed.feed.title, "http://media.npr.org/images/podcasts/2013/primary/hourly_news_summary.png", 0, None)) 45 | process(["mpg123", feed.entries[0].id]) 46 | elif event == "songlove": 47 | sendRequest(url, "SetSongInfo", buildJSON(title, artist, album, coverArt, rating, detailUrl)) 48 | elif event == "usergetstations" or event == "stationcreate" or event == "stationdelete" or event == "stationrename": # Code thanks to @officerNordBerg on GitHub 49 | stationCount = int(fields["stationCount"]) 50 | stations = "" 51 | for i in range(0, stationCount): 52 | stations += "%s="%i + fields["station%s"%i] + "|" 53 | stations = stations[0:len(stations) - 1] 54 | open(www + "stationList", "w").write(stations) -------------------------------------------------------------------------------- /cherrypy/_cpwsgi_server.py: -------------------------------------------------------------------------------- 1 | """WSGI server interface (see PEP 333). This adds some CP-specific bits to 2 | the framework-agnostic wsgiserver package. 3 | """ 4 | import sys 5 | 6 | import cherrypy 7 | from cherrypy import wsgiserver 8 | 9 | 10 | class CPWSGIServer(wsgiserver.CherryPyWSGIServer): 11 | """Wrapper for wsgiserver.CherryPyWSGIServer. 12 | 13 | wsgiserver has been designed to not reference CherryPy in any way, 14 | so that it can be used in other frameworks and applications. Therefore, 15 | we wrap it here, so we can set our own mount points from cherrypy.tree 16 | and apply some attributes from config -> cherrypy.server -> wsgiserver. 17 | """ 18 | 19 | def __init__(self, server_adapter=cherrypy.server): 20 | self.server_adapter = server_adapter 21 | self.max_request_header_size = self.server_adapter.max_request_header_size or 0 22 | self.max_request_body_size = self.server_adapter.max_request_body_size or 0 23 | 24 | server_name = (self.server_adapter.socket_host or 25 | self.server_adapter.socket_file or 26 | None) 27 | 28 | self.wsgi_version = self.server_adapter.wsgi_version 29 | s = wsgiserver.CherryPyWSGIServer 30 | s.__init__(self, server_adapter.bind_addr, cherrypy.tree, 31 | self.server_adapter.thread_pool, 32 | server_name, 33 | max = self.server_adapter.thread_pool_max, 34 | request_queue_size = self.server_adapter.socket_queue_size, 35 | timeout = self.server_adapter.socket_timeout, 36 | shutdown_timeout = self.server_adapter.shutdown_timeout, 37 | ) 38 | self.protocol = self.server_adapter.protocol_version 39 | self.nodelay = self.server_adapter.nodelay 40 | 41 | if sys.version_info >= (3, 0): 42 | ssl_module = self.server_adapter.ssl_module or 'builtin' 43 | else: 44 | ssl_module = self.server_adapter.ssl_module or 'pyopenssl' 45 | if self.server_adapter.ssl_context: 46 | adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module) 47 | self.ssl_adapter = adapter_class( 48 | self.server_adapter.ssl_certificate, 49 | self.server_adapter.ssl_private_key, 50 | self.server_adapter.ssl_certificate_chain) 51 | self.ssl_adapter.context = self.server_adapter.ssl_context 52 | elif self.server_adapter.ssl_certificate: 53 | adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module) 54 | self.ssl_adapter = adapter_class( 55 | self.server_adapter.ssl_certificate, 56 | self.server_adapter.ssl_private_key, 57 | self.server_adapter.ssl_certificate_chain) 58 | 59 | self.stats['Enabled'] = getattr(self.server_adapter, 'statistics', False) 60 | 61 | def error_log(self, msg="", level=20, traceback=False): 62 | cherrypy.engine.log(msg, level, traceback) 63 | 64 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | cd $HOME 2 | clear 3 | echo "Hello There!" 4 | echo "We're starting the Pidora Installation. Grab your helmets and hang on. This is going to be quick." 5 | echo 6 | echo "Installing packages. Now, this may take a while..." 7 | sudo apt-get install git mpg123 python-feedparser libao-dev libmad0-dev libfaad-dev libgnutls-dev libjson0-dev libgcrypt11-dev pkg-config python-setuptools libavfilter-dev libavformat-dev libavcodec-dev -y 8 | echo "Now configuring python environment..." 9 | wget https://raw.github.com/pypa/pip/master/contrib/get-pip.py 10 | sudo python get-pip.py 11 | sudo pip install requests 12 | echo "All packages installed" 13 | sleep 5 14 | clear 15 | echo "Cloning into pianobar" 16 | git clone https://github.com/PromyLOPh/pianobar.git -q 17 | echo "Cloning pianobar complete." 18 | cd pianobar/ 19 | echo "Building pianobar" 20 | sudo make install 21 | echo "Build complete" 22 | sleep 5 23 | clear 24 | echo "Starting to set up pianobar" 25 | read -p "What is your Pandora email address? " username 26 | read -p "What is your Pandora pasword? " password 27 | mkdir -p ~/.config/pianobar 28 | echo "user = $username 29 | password = $password" > ~/.config/pianobar/config 30 | fingerprint=`openssl s_client -connect tuner.pandora.com:443 < /dev/null 2> /dev/null | openssl x509 -noout -fingerprint | tr -d ':' | cut -d'=' -f2` && echo tls_fingerprint = $fingerprint >> ~/.config/pianobar/config 31 | echo 32 | echo "We think we set up pianobar. We'll test it now. It should log in and ask you to select a station." 33 | echo "After selecting the station, pianobar will print the station's name and a long ID number. This will be the default station. Copy that number to the clipboard." 34 | echo "Press q to quit at any time." 35 | read -n1 -r -p "Press any key to continue..." 36 | echo 37 | pianobar 38 | echo 39 | echo "If for whatever reason it didn't work, you'll need to troubleshoot it yourself." 40 | echo "Once you've done so, we'll start setting up the rest of it." 41 | read -n1 -r -p "Press any key to continue..." 42 | echo 43 | read -p "What was the station's ID number? " stationID 44 | echo "autostart_station = $stationID" >> ~/.config/pianobar/config 45 | cd $HOME 46 | clear 47 | echo "Thanks. We'll now start cloning into Pidora" 48 | git clone https://github.com/jacroe/pidora.git -q 49 | echo "Cloning Pidora complete." 50 | sed -i "s,/home/pi,$HOME," cpy.conf 51 | echo 52 | echo "We'll begin setting up Pidora for use." 53 | echo "Creating FIFO queue" 54 | mkfifo pidora/ctl 55 | echo "Adding new variables to pianobar's config" 56 | echo "event_command = $HOME/pidora/bar-update.py 57 | fifo = $HOME/pidora/ctl" >> ~/.config/pianobar/config 58 | echo "We think we're done. Let's test it, shall we?" 59 | read -n1 -r -p "Press any key to continue..." 60 | clear 61 | LANip=`ip addr show | awk '$1=="inet" {print $2}' | cut -f1 -d'/'` 62 | echo "We're going to start the Pidora webserver now." 63 | echo "You can access it by going to http://127.0.0.1:8080 in the RPi's browser or" 64 | echo "http://"$LANip":8080 on a device on the same Local Area Network." 65 | echo "You should have full control of pianobar. Experiment with it to be sure." 66 | echo "You can stop the server at any time by pressing Ctrl+C." 67 | python pidora/hello.py 68 | clear 69 | echo "Configuring the startup script." 70 | echo "rm $HOME/pidora/curSong.json 71 | @python $HOME/pidora/hello.py 72 | @midori -a \"http://localhost:8080/\" -e Fullscreen" | sudo tee -a /etc/xdg/lxsession/LXDE/autostart 73 | clear 74 | echo "Done!" 75 | echo "Reboot your Pi and enjoy!" 76 | -------------------------------------------------------------------------------- /static/css/mobile.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin:0; 3 | padding:0; 4 | border:0; /* This removes the border around the viewport in old versions of IE */ 5 | width:100%; 6 | min-width:300px; /* Minimum width of layout - remove line if not required */ 7 | background:#101a4d; 8 | /* The min-width property does not work in old versions of Internet Explorer */ 9 | font-size:90%; 10 | font-family: Arial, sans-serif; 11 | overflow:hidden; 12 | 13 | } 14 | .displayed { 15 | display: block; 16 | margin-left: auto; 17 | margin-right: auto; 18 | text-align: center; 19 | } 20 | 21 | a { 22 | color:#fff; 23 | } 24 | a:hover { 25 | color:#369; 26 | background:#fff; 27 | text-decoration:none; 28 | } 29 | h1, h2, h3 { 30 | margin:.8em 0 .2em 0; 31 | padding:0; 32 | color:#fff; 33 | } 34 | p { 35 | margin:.4em 0 .8em 0; 36 | padding:0; 37 | color: #c0c0c0; 38 | } 39 | img { 40 | margin-left:auto; 41 | margin-right:auto; 42 | max-width: 90%; 43 | height: auto; 44 | width: auto\9; /*for ie8 */ 45 | } 46 | /* Header styles */ 47 | #header { 48 | width:100%; 49 | display: inline-block; 50 | text-align:center; 51 | border-bottom:4px solid #000; 52 | } 53 | #header ul { 54 | width:100%; 55 | list-style:none; 56 | margin:10px 0 0 0; 57 | padding:0; 58 | } 59 | #header ul li { 60 | display:inline-block; 61 | list-style:none; 62 | margin:0; 63 | padding:0; 64 | } 65 | #header ul li a { 66 | margin:0 0 0 1px; 67 | padding:3px 10px; 68 | text-align:center; 69 | color:#000; 70 | text-decoration:none; 71 | position:relative; 72 | line-height:1.3em; 73 | background:#eee; 74 | } 75 | #header ul li a:hover { 76 | background:#369; 77 | color:#fff; 78 | } 79 | #header ul li a.active, 80 | #header ul li a.active:hover { 81 | color:#fff; 82 | background:#000; 83 | font-weight:bold; 84 | } 85 | /* 'widths' sub menu */ 86 | /* column container */ 87 | .colmask { 88 | position:relative; /* This fixes the IE7 overflow hidden bug */ 89 | clear:both; 90 | float:left; 91 | width:100%; /* width of whole page */ 92 | overflow:hidden; /* This chops off any overhanging divs */ 93 | } 94 | /* common column settings */ 95 | .colright, 96 | .colmid, 97 | .colleft { 98 | float:left; 99 | width:100%; 100 | position:relative; 101 | } 102 | .col1, 103 | .col2, 104 | .col3 { 105 | float:left; 106 | position:relative; 107 | padding:0 0 1em 0; 108 | overflow:hidden; 109 | } 110 | /* Full page settings */ 111 | .fullpage { 112 | background:#c0c0c0; /* page background colour */ 113 | } 114 | .fullpage .col1 { 115 | width:100%; /* page width minus left and right padding */ 116 | } 117 | /* Footer styles */ 118 | #footer { 119 | width:100%; 120 | border-top:1px solid #000; 121 | } 122 | /* --> */ 123 | -------------------------------------------------------------------------------- /cherrypy/wsgiserver/ssl_builtin.py: -------------------------------------------------------------------------------- 1 | """A library for integrating Python's builtin ``ssl`` library with CherryPy. 2 | 3 | The ssl module must be importable for SSL functionality. 4 | 5 | To use this module, set ``CherryPyWSGIServer.ssl_adapter`` to an instance of 6 | ``BuiltinSSLAdapter``. 7 | """ 8 | 9 | try: 10 | import ssl 11 | except ImportError: 12 | ssl = None 13 | 14 | try: 15 | from _pyio import DEFAULT_BUFFER_SIZE 16 | except ImportError: 17 | try: 18 | from io import DEFAULT_BUFFER_SIZE 19 | except ImportError: 20 | DEFAULT_BUFFER_SIZE = -1 21 | 22 | import sys 23 | 24 | from cherrypy import wsgiserver 25 | 26 | 27 | class BuiltinSSLAdapter(wsgiserver.SSLAdapter): 28 | """A wrapper for integrating Python's builtin ssl module with CherryPy.""" 29 | 30 | certificate = None 31 | """The filename of the server SSL certificate.""" 32 | 33 | private_key = None 34 | """The filename of the server's private key file.""" 35 | 36 | def __init__(self, certificate, private_key, certificate_chain=None): 37 | if ssl is None: 38 | raise ImportError("You must install the ssl module to use HTTPS.") 39 | self.certificate = certificate 40 | self.private_key = private_key 41 | self.certificate_chain = certificate_chain 42 | 43 | def bind(self, sock): 44 | """Wrap and return the given socket.""" 45 | return sock 46 | 47 | def wrap(self, sock): 48 | """Wrap and return the given socket, plus WSGI environ entries.""" 49 | try: 50 | s = ssl.wrap_socket(sock, do_handshake_on_connect=True, 51 | server_side=True, certfile=self.certificate, 52 | keyfile=self.private_key, ssl_version=ssl.PROTOCOL_SSLv23) 53 | except ssl.SSLError: 54 | e = sys.exc_info()[1] 55 | if e.errno == ssl.SSL_ERROR_EOF: 56 | # This is almost certainly due to the cherrypy engine 57 | # 'pinging' the socket to assert it's connectable; 58 | # the 'ping' isn't SSL. 59 | return None, {} 60 | elif e.errno == ssl.SSL_ERROR_SSL: 61 | if e.args[1].endswith('http request'): 62 | # The client is speaking HTTP to an HTTPS server. 63 | raise wsgiserver.NoSSLError 64 | elif e.args[1].endswith('unknown protocol'): 65 | # The client is speaking some non-HTTP protocol. 66 | # Drop the conn. 67 | return None, {} 68 | raise 69 | return s, self.get_environ(s) 70 | 71 | # TODO: fill this out more with mod ssl env 72 | def get_environ(self, sock): 73 | """Create WSGI environ entries to be merged into each request.""" 74 | cipher = sock.cipher() 75 | ssl_environ = { 76 | "wsgi.url_scheme": "https", 77 | "HTTPS": "on", 78 | 'SSL_PROTOCOL': cipher[1], 79 | 'SSL_CIPHER': cipher[0] 80 | ## SSL_VERSION_INTERFACE string The mod_ssl program version 81 | ## SSL_VERSION_LIBRARY string The OpenSSL program version 82 | } 83 | return ssl_environ 84 | 85 | if sys.version_info >= (3, 0): 86 | def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE): 87 | return wsgiserver.CP_makefile(sock, mode, bufsize) 88 | else: 89 | def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE): 90 | return wsgiserver.CP_fileobject(sock, mode, bufsize) 91 | 92 | -------------------------------------------------------------------------------- /cherrypy/lib/auth.py: -------------------------------------------------------------------------------- 1 | import cherrypy 2 | from cherrypy.lib import httpauth 3 | 4 | 5 | def check_auth(users, encrypt=None, realm=None): 6 | """If an authorization header contains credentials, return True, else False.""" 7 | request = cherrypy.serving.request 8 | if 'authorization' in request.headers: 9 | # make sure the provided credentials are correctly set 10 | ah = httpauth.parseAuthorization(request.headers['authorization']) 11 | if ah is None: 12 | raise cherrypy.HTTPError(400, 'Bad Request') 13 | 14 | if not encrypt: 15 | encrypt = httpauth.DIGEST_AUTH_ENCODERS[httpauth.MD5] 16 | 17 | if hasattr(users, '__call__'): 18 | try: 19 | # backward compatibility 20 | users = users() # expect it to return a dictionary 21 | 22 | if not isinstance(users, dict): 23 | raise ValueError("Authentication users must be a dictionary") 24 | 25 | # fetch the user password 26 | password = users.get(ah["username"], None) 27 | except TypeError: 28 | # returns a password (encrypted or clear text) 29 | password = users(ah["username"]) 30 | else: 31 | if not isinstance(users, dict): 32 | raise ValueError("Authentication users must be a dictionary") 33 | 34 | # fetch the user password 35 | password = users.get(ah["username"], None) 36 | 37 | # validate the authorization by re-computing it here 38 | # and compare it with what the user-agent provided 39 | if httpauth.checkResponse(ah, password, method=request.method, 40 | encrypt=encrypt, realm=realm): 41 | request.login = ah["username"] 42 | return True 43 | 44 | request.login = False 45 | return False 46 | 47 | def basic_auth(realm, users, encrypt=None, debug=False): 48 | """If auth fails, raise 401 with a basic authentication header. 49 | 50 | realm 51 | A string containing the authentication realm. 52 | 53 | users 54 | A dict of the form: {username: password} or a callable returning a dict. 55 | 56 | encrypt 57 | callable used to encrypt the password returned from the user-agent. 58 | if None it defaults to a md5 encryption. 59 | 60 | """ 61 | if check_auth(users, encrypt): 62 | if debug: 63 | cherrypy.log('Auth successful', 'TOOLS.BASIC_AUTH') 64 | return 65 | 66 | # inform the user-agent this path is protected 67 | cherrypy.serving.response.headers['www-authenticate'] = httpauth.basicAuth(realm) 68 | 69 | raise cherrypy.HTTPError(401, "You are not authorized to access that resource") 70 | 71 | def digest_auth(realm, users, debug=False): 72 | """If auth fails, raise 401 with a digest authentication header. 73 | 74 | realm 75 | A string containing the authentication realm. 76 | users 77 | A dict of the form: {username: password} or a callable returning a dict. 78 | """ 79 | if check_auth(users, realm=realm): 80 | if debug: 81 | cherrypy.log('Auth successful', 'TOOLS.DIGEST_AUTH') 82 | return 83 | 84 | # inform the user-agent this path is protected 85 | cherrypy.serving.response.headers['www-authenticate'] = httpauth.digestAuth(realm) 86 | 87 | raise cherrypy.HTTPError(401, "You are not authorized to access that resource") 88 | -------------------------------------------------------------------------------- /cherrypy/lib/auth_basic.py: -------------------------------------------------------------------------------- 1 | # This file is part of CherryPy 2 | # -*- coding: utf-8 -*- 3 | # vim:ts=4:sw=4:expandtab:fileencoding=utf-8 4 | 5 | __doc__ = """This module provides a CherryPy 3.x tool which implements 6 | the server-side of HTTP Basic Access Authentication, as described in :rfc:`2617`. 7 | 8 | Example usage, using the built-in checkpassword_dict function which uses a dict 9 | as the credentials store:: 10 | 11 | userpassdict = {'bird' : 'bebop', 'ornette' : 'wayout'} 12 | checkpassword = cherrypy.lib.auth_basic.checkpassword_dict(userpassdict) 13 | basic_auth = {'tools.auth_basic.on': True, 14 | 'tools.auth_basic.realm': 'earth', 15 | 'tools.auth_basic.checkpassword': checkpassword, 16 | } 17 | app_config = { '/' : basic_auth } 18 | 19 | """ 20 | 21 | __author__ = 'visteya' 22 | __date__ = 'April 2009' 23 | 24 | import binascii 25 | from cherrypy._cpcompat import base64_decode 26 | import cherrypy 27 | 28 | 29 | def checkpassword_dict(user_password_dict): 30 | """Returns a checkpassword function which checks credentials 31 | against a dictionary of the form: {username : password}. 32 | 33 | If you want a simple dictionary-based authentication scheme, use 34 | checkpassword_dict(my_credentials_dict) as the value for the 35 | checkpassword argument to basic_auth(). 36 | """ 37 | def checkpassword(realm, user, password): 38 | p = user_password_dict.get(user) 39 | return p and p == password or False 40 | 41 | return checkpassword 42 | 43 | 44 | def basic_auth(realm, checkpassword, debug=False): 45 | """A CherryPy tool which hooks at before_handler to perform 46 | HTTP Basic Access Authentication, as specified in :rfc:`2617`. 47 | 48 | If the request has an 'authorization' header with a 'Basic' scheme, this 49 | tool attempts to authenticate the credentials supplied in that header. If 50 | the request has no 'authorization' header, or if it does but the scheme is 51 | not 'Basic', or if authentication fails, the tool sends a 401 response with 52 | a 'WWW-Authenticate' Basic header. 53 | 54 | realm 55 | A string containing the authentication realm. 56 | 57 | checkpassword 58 | A callable which checks the authentication credentials. 59 | Its signature is checkpassword(realm, username, password). where 60 | username and password are the values obtained from the request's 61 | 'authorization' header. If authentication succeeds, checkpassword 62 | returns True, else it returns False. 63 | 64 | """ 65 | 66 | if '"' in realm: 67 | raise ValueError('Realm cannot contain the " (quote) character.') 68 | request = cherrypy.serving.request 69 | 70 | auth_header = request.headers.get('authorization') 71 | if auth_header is not None: 72 | try: 73 | scheme, params = auth_header.split(' ', 1) 74 | if scheme.lower() == 'basic': 75 | username, password = base64_decode(params).split(':', 1) 76 | if checkpassword(realm, username, password): 77 | if debug: 78 | cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC') 79 | request.login = username 80 | return # successful authentication 81 | except (ValueError, binascii.Error): # split() error, base64.decodestring() error 82 | raise cherrypy.HTTPError(400, 'Bad Request') 83 | 84 | # Respond with 401 status and a WWW-Authenticate header 85 | cherrypy.serving.response.headers['www-authenticate'] = 'Basic realm="%s"' % realm 86 | raise cherrypy.HTTPError(401, "You are not authorized to access that resource") 87 | 88 | -------------------------------------------------------------------------------- /cherrypy/lib/jsontools.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import cherrypy 3 | from cherrypy._cpcompat import basestring, ntou, json, json_encode, json_decode 4 | 5 | def json_processor(entity): 6 | """Read application/json data into request.json.""" 7 | if not entity.headers.get(ntou("Content-Length"), ntou("")): 8 | raise cherrypy.HTTPError(411) 9 | 10 | body = entity.fp.read() 11 | try: 12 | cherrypy.serving.request.json = json_decode(body.decode('utf-8')) 13 | except ValueError: 14 | raise cherrypy.HTTPError(400, 'Invalid JSON document') 15 | 16 | def json_in(content_type=[ntou('application/json'), ntou('text/javascript')], 17 | force=True, debug=False, processor = json_processor): 18 | """Add a processor to parse JSON request entities: 19 | The default processor places the parsed data into request.json. 20 | 21 | Incoming request entities which match the given content_type(s) will 22 | be deserialized from JSON to the Python equivalent, and the result 23 | stored at cherrypy.request.json. The 'content_type' argument may 24 | be a Content-Type string or a list of allowable Content-Type strings. 25 | 26 | If the 'force' argument is True (the default), then entities of other 27 | content types will not be allowed; "415 Unsupported Media Type" is 28 | raised instead. 29 | 30 | Supply your own processor to use a custom decoder, or to handle the parsed 31 | data differently. The processor can be configured via 32 | tools.json_in.processor or via the decorator method. 33 | 34 | Note that the deserializer requires the client send a Content-Length 35 | request header, or it will raise "411 Length Required". If for any 36 | other reason the request entity cannot be deserialized from JSON, 37 | it will raise "400 Bad Request: Invalid JSON document". 38 | 39 | You must be using Python 2.6 or greater, or have the 'simplejson' 40 | package importable; otherwise, ValueError is raised during processing. 41 | """ 42 | request = cherrypy.serving.request 43 | if isinstance(content_type, basestring): 44 | content_type = [content_type] 45 | 46 | if force: 47 | if debug: 48 | cherrypy.log('Removing body processors %s' % 49 | repr(request.body.processors.keys()), 'TOOLS.JSON_IN') 50 | request.body.processors.clear() 51 | request.body.default_proc = cherrypy.HTTPError( 52 | 415, 'Expected an entity of content type %s' % 53 | ', '.join(content_type)) 54 | 55 | for ct in content_type: 56 | if debug: 57 | cherrypy.log('Adding body processor for %s' % ct, 'TOOLS.JSON_IN') 58 | request.body.processors[ct] = processor 59 | 60 | def json_handler(*args, **kwargs): 61 | value = cherrypy.serving.request._json_inner_handler(*args, **kwargs) 62 | return json_encode(value) 63 | 64 | def json_out(content_type='application/json', debug=False, handler=json_handler): 65 | """Wrap request.handler to serialize its output to JSON. Sets Content-Type. 66 | 67 | If the given content_type is None, the Content-Type response header 68 | is not set. 69 | 70 | Provide your own handler to use a custom encoder. For example 71 | cherrypy.config['tools.json_out.handler'] = , or 72 | @json_out(handler=function). 73 | 74 | You must be using Python 2.6 or greater, or have the 'simplejson' 75 | package importable; otherwise, ValueError is raised during processing. 76 | """ 77 | request = cherrypy.serving.request 78 | if debug: 79 | cherrypy.log('Replacing %s with JSON handler' % request.handler, 80 | 'TOOLS.JSON_OUT') 81 | request._json_inner_handler = request.handler 82 | request.handler = handler 83 | if content_type is not None: 84 | if debug: 85 | cherrypy.log('Setting Content-Type to %s' % content_type, 'TOOLS.JSON_OUT') 86 | cherrypy.serving.response.headers['Content-Type'] = content_type 87 | 88 | -------------------------------------------------------------------------------- /cherrypy/cherryd: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | """The CherryPy daemon.""" 3 | 4 | import sys 5 | 6 | import cherrypy 7 | from cherrypy.process import plugins, servers 8 | from cherrypy import Application 9 | 10 | def start(configfiles=None, daemonize=False, environment=None, 11 | fastcgi=False, scgi=False, pidfile=None, imports=None, 12 | cgi=False): 13 | """Subscribe all engine plugins and start the engine.""" 14 | sys.path = [''] + sys.path 15 | for i in imports or []: 16 | exec("import %s" % i) 17 | 18 | for c in configfiles or []: 19 | cherrypy.config.update(c) 20 | # If there's only one app mounted, merge config into it. 21 | if len(cherrypy.tree.apps) == 1: 22 | for app in cherrypy.tree.apps.values(): 23 | if isinstance(app, Application): 24 | app.merge(c) 25 | 26 | engine = cherrypy.engine 27 | 28 | if environment is not None: 29 | cherrypy.config.update({'environment': environment}) 30 | 31 | # Only daemonize if asked to. 32 | if daemonize: 33 | # Don't print anything to stdout/sterr. 34 | cherrypy.config.update({'log.screen': False}) 35 | plugins.Daemonizer(engine).subscribe() 36 | 37 | if pidfile: 38 | plugins.PIDFile(engine, pidfile).subscribe() 39 | 40 | if hasattr(engine, "signal_handler"): 41 | engine.signal_handler.subscribe() 42 | if hasattr(engine, "console_control_handler"): 43 | engine.console_control_handler.subscribe() 44 | 45 | if (fastcgi and (scgi or cgi)) or (scgi and cgi): 46 | cherrypy.log.error("You may only specify one of the cgi, fastcgi, and " 47 | "scgi options.", 'ENGINE') 48 | sys.exit(1) 49 | elif fastcgi or scgi or cgi: 50 | # Turn off autoreload when using *cgi. 51 | cherrypy.config.update({'engine.autoreload_on': False}) 52 | # Turn off the default HTTP server (which is subscribed by default). 53 | cherrypy.server.unsubscribe() 54 | 55 | addr = cherrypy.server.bind_addr 56 | if fastcgi: 57 | f = servers.FlupFCGIServer(application=cherrypy.tree, 58 | bindAddress=addr) 59 | elif scgi: 60 | f = servers.FlupSCGIServer(application=cherrypy.tree, 61 | bindAddress=addr) 62 | else: 63 | f = servers.FlupCGIServer(application=cherrypy.tree, 64 | bindAddress=addr) 65 | s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr) 66 | s.subscribe() 67 | 68 | # Always start the engine; this will start all other services 69 | try: 70 | engine.start() 71 | except: 72 | # Assume the error has been logged already via bus.log. 73 | sys.exit(1) 74 | else: 75 | engine.block() 76 | 77 | 78 | if __name__ == '__main__': 79 | from optparse import OptionParser 80 | 81 | p = OptionParser() 82 | p.add_option('-c', '--config', action="append", dest='config', 83 | help="specify config file(s)") 84 | p.add_option('-d', action="store_true", dest='daemonize', 85 | help="run the server as a daemon") 86 | p.add_option('-e', '--environment', dest='environment', default=None, 87 | help="apply the given config environment") 88 | p.add_option('-f', action="store_true", dest='fastcgi', 89 | help="start a fastcgi server instead of the default HTTP server") 90 | p.add_option('-s', action="store_true", dest='scgi', 91 | help="start a scgi server instead of the default HTTP server") 92 | p.add_option('-x', action="store_true", dest='cgi', 93 | help="start a cgi server instead of the default HTTP server") 94 | p.add_option('-i', '--import', action="append", dest='imports', 95 | help="specify modules to import") 96 | p.add_option('-p', '--pidfile', dest='pidfile', default=None, 97 | help="store the process id in the given file") 98 | p.add_option('-P', '--Path', action="append", dest='Path', 99 | help="add the given paths to sys.path") 100 | options, args = p.parse_args() 101 | 102 | if options.Path: 103 | for p in options.Path: 104 | sys.path.insert(0, p) 105 | 106 | start(options.config, options.daemonize, 107 | options.environment, options.fastcgi, options.scgi, 108 | options.pidfile, options.imports, options.cgi) 109 | 110 | -------------------------------------------------------------------------------- /cherrypy/process/win32.py: -------------------------------------------------------------------------------- 1 | """Windows service. Requires pywin32.""" 2 | 3 | import os 4 | import win32api 5 | import win32con 6 | import win32event 7 | import win32service 8 | import win32serviceutil 9 | 10 | from cherrypy.process import wspbus, plugins 11 | 12 | 13 | class ConsoleCtrlHandler(plugins.SimplePlugin): 14 | """A WSPBus plugin for handling Win32 console events (like Ctrl-C).""" 15 | 16 | def __init__(self, bus): 17 | self.is_set = False 18 | plugins.SimplePlugin.__init__(self, bus) 19 | 20 | def start(self): 21 | if self.is_set: 22 | self.bus.log('Handler for console events already set.', level=40) 23 | return 24 | 25 | result = win32api.SetConsoleCtrlHandler(self.handle, 1) 26 | if result == 0: 27 | self.bus.log('Could not SetConsoleCtrlHandler (error %r)' % 28 | win32api.GetLastError(), level=40) 29 | else: 30 | self.bus.log('Set handler for console events.', level=40) 31 | self.is_set = True 32 | 33 | def stop(self): 34 | if not self.is_set: 35 | self.bus.log('Handler for console events already off.', level=40) 36 | return 37 | 38 | try: 39 | result = win32api.SetConsoleCtrlHandler(self.handle, 0) 40 | except ValueError: 41 | # "ValueError: The object has not been registered" 42 | result = 1 43 | 44 | if result == 0: 45 | self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' % 46 | win32api.GetLastError(), level=40) 47 | else: 48 | self.bus.log('Removed handler for console events.', level=40) 49 | self.is_set = False 50 | 51 | def handle(self, event): 52 | """Handle console control events (like Ctrl-C).""" 53 | if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT, 54 | win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT, 55 | win32con.CTRL_CLOSE_EVENT): 56 | self.bus.log('Console event %s: shutting down bus' % event) 57 | 58 | # Remove self immediately so repeated Ctrl-C doesn't re-call it. 59 | try: 60 | self.stop() 61 | except ValueError: 62 | pass 63 | 64 | self.bus.exit() 65 | # 'First to return True stops the calls' 66 | return 1 67 | return 0 68 | 69 | 70 | class Win32Bus(wspbus.Bus): 71 | """A Web Site Process Bus implementation for Win32. 72 | 73 | Instead of time.sleep, this bus blocks using native win32event objects. 74 | """ 75 | 76 | def __init__(self): 77 | self.events = {} 78 | wspbus.Bus.__init__(self) 79 | 80 | def _get_state_event(self, state): 81 | """Return a win32event for the given state (creating it if needed).""" 82 | try: 83 | return self.events[state] 84 | except KeyError: 85 | event = win32event.CreateEvent(None, 0, 0, 86 | "WSPBus %s Event (pid=%r)" % 87 | (state.name, os.getpid())) 88 | self.events[state] = event 89 | return event 90 | 91 | def _get_state(self): 92 | return self._state 93 | def _set_state(self, value): 94 | self._state = value 95 | event = self._get_state_event(value) 96 | win32event.PulseEvent(event) 97 | state = property(_get_state, _set_state) 98 | 99 | def wait(self, state, interval=0.1, channel=None): 100 | """Wait for the given state(s), KeyboardInterrupt or SystemExit. 101 | 102 | Since this class uses native win32event objects, the interval 103 | argument is ignored. 104 | """ 105 | if isinstance(state, (tuple, list)): 106 | # Don't wait for an event that beat us to the punch ;) 107 | if self.state not in state: 108 | events = tuple([self._get_state_event(s) for s in state]) 109 | win32event.WaitForMultipleObjects(events, 0, win32event.INFINITE) 110 | else: 111 | # Don't wait for an event that beat us to the punch ;) 112 | if self.state != state: 113 | event = self._get_state_event(state) 114 | win32event.WaitForSingleObject(event, win32event.INFINITE) 115 | 116 | 117 | class _ControlCodes(dict): 118 | """Control codes used to "signal" a service via ControlService. 119 | 120 | User-defined control codes are in the range 128-255. We generally use 121 | the standard Python value for the Linux signal and add 128. Example: 122 | 123 | >>> signal.SIGUSR1 124 | 10 125 | control_codes['graceful'] = 128 + 10 126 | """ 127 | 128 | def key_for(self, obj): 129 | """For the given value, return its corresponding key.""" 130 | for key, val in self.items(): 131 | if val is obj: 132 | return key 133 | raise ValueError("The given object could not be found: %r" % obj) 134 | 135 | control_codes = _ControlCodes({'graceful': 138}) 136 | 137 | 138 | def signal_child(service, command): 139 | if command == 'stop': 140 | win32serviceutil.StopService(service) 141 | elif command == 'restart': 142 | win32serviceutil.RestartService(service) 143 | else: 144 | win32serviceutil.ControlService(service, control_codes[command]) 145 | 146 | 147 | class PyWebService(win32serviceutil.ServiceFramework): 148 | """Python Web Service.""" 149 | 150 | _svc_name_ = "Python Web Service" 151 | _svc_display_name_ = "Python Web Service" 152 | _svc_deps_ = None # sequence of service names on which this depends 153 | _exe_name_ = "pywebsvc" 154 | _exe_args_ = None # Default to no arguments 155 | 156 | # Only exists on Windows 2000 or later, ignored on windows NT 157 | _svc_description_ = "Python Web Service" 158 | 159 | def SvcDoRun(self): 160 | from cherrypy import process 161 | process.bus.start() 162 | process.bus.block() 163 | 164 | def SvcStop(self): 165 | from cherrypy import process 166 | self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) 167 | process.bus.exit() 168 | 169 | def SvcOther(self, control): 170 | process.bus.publish(control_codes.key_for(control)) 171 | 172 | 173 | if __name__ == '__main__': 174 | win32serviceutil.HandleCommandLine(PyWebService) 175 | -------------------------------------------------------------------------------- /cherrypy/_cpnative_server.py: -------------------------------------------------------------------------------- 1 | """Native adapter for serving CherryPy via its builtin server.""" 2 | 3 | import logging 4 | import sys 5 | 6 | import cherrypy 7 | from cherrypy._cpcompat import BytesIO 8 | from cherrypy._cperror import format_exc, bare_error 9 | from cherrypy.lib import httputil 10 | from cherrypy import wsgiserver 11 | 12 | 13 | class NativeGateway(wsgiserver.Gateway): 14 | 15 | recursive = False 16 | 17 | def respond(self): 18 | req = self.req 19 | try: 20 | # Obtain a Request object from CherryPy 21 | local = req.server.bind_addr 22 | local = httputil.Host(local[0], local[1], "") 23 | remote = req.conn.remote_addr, req.conn.remote_port 24 | remote = httputil.Host(remote[0], remote[1], "") 25 | 26 | scheme = req.scheme 27 | sn = cherrypy.tree.script_name(req.uri or "/") 28 | if sn is None: 29 | self.send_response('404 Not Found', [], ['']) 30 | else: 31 | app = cherrypy.tree.apps[sn] 32 | method = req.method 33 | path = req.path 34 | qs = req.qs or "" 35 | headers = req.inheaders.items() 36 | rfile = req.rfile 37 | prev = None 38 | 39 | try: 40 | redirections = [] 41 | while True: 42 | request, response = app.get_serving( 43 | local, remote, scheme, "HTTP/1.1") 44 | request.multithread = True 45 | request.multiprocess = False 46 | request.app = app 47 | request.prev = prev 48 | 49 | # Run the CherryPy Request object and obtain the response 50 | try: 51 | request.run(method, path, qs, req.request_protocol, headers, rfile) 52 | break 53 | except cherrypy.InternalRedirect: 54 | ir = sys.exc_info()[1] 55 | app.release_serving() 56 | prev = request 57 | 58 | if not self.recursive: 59 | if ir.path in redirections: 60 | raise RuntimeError("InternalRedirector visited the " 61 | "same URL twice: %r" % ir.path) 62 | else: 63 | # Add the *previous* path_info + qs to redirections. 64 | if qs: 65 | qs = "?" + qs 66 | redirections.append(sn + path + qs) 67 | 68 | # Munge environment and try again. 69 | method = "GET" 70 | path = ir.path 71 | qs = ir.query_string 72 | rfile = BytesIO() 73 | 74 | self.send_response( 75 | response.output_status, response.header_list, 76 | response.body) 77 | finally: 78 | app.release_serving() 79 | except: 80 | tb = format_exc() 81 | #print tb 82 | cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR) 83 | s, h, b = bare_error() 84 | self.send_response(s, h, b) 85 | 86 | def send_response(self, status, headers, body): 87 | req = self.req 88 | 89 | # Set response status 90 | req.status = str(status or "500 Server Error") 91 | 92 | # Set response headers 93 | for header, value in headers: 94 | req.outheaders.append((header, value)) 95 | if (req.ready and not req.sent_headers): 96 | req.sent_headers = True 97 | req.send_headers() 98 | 99 | # Set response body 100 | for seg in body: 101 | req.write(seg) 102 | 103 | 104 | class CPHTTPServer(wsgiserver.HTTPServer): 105 | """Wrapper for wsgiserver.HTTPServer. 106 | 107 | wsgiserver has been designed to not reference CherryPy in any way, 108 | so that it can be used in other frameworks and applications. 109 | Therefore, we wrap it here, so we can apply some attributes 110 | from config -> cherrypy.server -> HTTPServer. 111 | """ 112 | 113 | def __init__(self, server_adapter=cherrypy.server): 114 | self.server_adapter = server_adapter 115 | 116 | server_name = (self.server_adapter.socket_host or 117 | self.server_adapter.socket_file or 118 | None) 119 | 120 | wsgiserver.HTTPServer.__init__( 121 | self, server_adapter.bind_addr, NativeGateway, 122 | minthreads=server_adapter.thread_pool, 123 | maxthreads=server_adapter.thread_pool_max, 124 | server_name=server_name) 125 | 126 | self.max_request_header_size = self.server_adapter.max_request_header_size or 0 127 | self.max_request_body_size = self.server_adapter.max_request_body_size or 0 128 | self.request_queue_size = self.server_adapter.socket_queue_size 129 | self.timeout = self.server_adapter.socket_timeout 130 | self.shutdown_timeout = self.server_adapter.shutdown_timeout 131 | self.protocol = self.server_adapter.protocol_version 132 | self.nodelay = self.server_adapter.nodelay 133 | 134 | ssl_module = self.server_adapter.ssl_module or 'pyopenssl' 135 | if self.server_adapter.ssl_context: 136 | adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module) 137 | self.ssl_adapter = adapter_class( 138 | self.server_adapter.ssl_certificate, 139 | self.server_adapter.ssl_private_key, 140 | self.server_adapter.ssl_certificate_chain) 141 | self.ssl_adapter.context = self.server_adapter.ssl_context 142 | elif self.server_adapter.ssl_certificate: 143 | adapter_class = wsgiserver.get_ssl_adapter_class(ssl_module) 144 | self.ssl_adapter = adapter_class( 145 | self.server_adapter.ssl_certificate, 146 | self.server_adapter.ssl_private_key, 147 | self.server_adapter.ssl_certificate_chain) 148 | 149 | 150 | -------------------------------------------------------------------------------- /static/js/pidora.js: -------------------------------------------------------------------------------- 1 | $(document).ready(function() 2 | { 3 | var newDataPlain, oldSongData, newSongData; 4 | window.setInterval(function() 5 | { 6 | $.get("api", {json:JSON.stringify({"method":"GetSongInfo", "id":1})}).done(function(newData) 7 | { 8 | if(!newData.msg) 9 | { 10 | if($('#msg').is(':visible')) 11 | { 12 | clearScreen('#content'); 13 | } 14 | newSongData = newData.song; 15 | if(JSON.stringify(oldSongData) !== JSON.stringify(newSongData)) 16 | { 17 | oldSongData = newSongData; 18 | clearScreen('#content', function() 19 | { 20 | updateSong(newSongData); 21 | }); 22 | if(newSongData.isSong) 23 | { 24 | setMousetraps(); 25 | $('#controls').fadeIn('slow'); 26 | } 27 | else 28 | { 29 | Mousetrap.reset(); 30 | $('#controls').fadeOut('slow'); 31 | if(newSongData.startup == false) 32 | Mousetrap.bind(['s', 'enter', 'space'], function() { pianobarStart(); }); 33 | } 34 | } 35 | } 36 | else if(newData.msg) 37 | { 38 | clearScreen('#msg', function() 39 | { 40 | $('#msg h1').html(newData.msg); 41 | }); 42 | } 43 | }); 44 | }, 3000); 45 | }); 46 | 47 | function clearScreen(showNext, doNext) 48 | { 49 | $('#content, #msg, #newStation, #stationList').fadeOut('slow').promise().done(function() 50 | { 51 | if(doNext) 52 | doNext(); 53 | $(showNext).fadeIn('slow'); 54 | }); 55 | } 56 | 57 | function updateSong(data) 58 | { 59 | if(data.startup == false) 60 | { 61 | document.title = "Pidora"; 62 | $('#content h1').html("Want Music?"); 63 | $('#content h2').html("Scan the QR Code"); 64 | $('#content .album').html(""); 65 | $('#content .love').hide(); 66 | $('#content .details').html("EMPTY").hide(); 67 | $('#content .albumart').attr("src", "https://chart.googleapis.com/chart?cht=qr&chs=500x500&chl=http%3A//" + location.host + "/start&chld=H|0"); 68 | } 69 | else if(data.startup == true) 70 | { 71 | document.title = "Starting up | Pidora"; 72 | $('#content h1').html("Hello There"); 73 | $('#content h2').html("Pianobar is starting up..."); 74 | $('#content .album').html(""); 75 | $('#content .love').hide(); 76 | $('#content .albumart').attr("src", "imgs/pandora.png"); 77 | } 78 | else 79 | { 80 | document.title = data.title + ", " + data.artist + " | Pidora"; 81 | $('#content h1').html(data.title); 82 | $('#content h2').html(data.artist); 83 | $('#content .album').html(data.album); 84 | $('#content .details').html("EMPTY").hide(); 85 | if(data.loved) 86 | $('#content .love').show(); 87 | else 88 | $('#content .love').hide(); 89 | $('#content .albumart').attr("alt", data.album + " by " + data.artist); 90 | if(data.artURL) 91 | $('#content .albumart').attr("src", data.artURL); 92 | else 93 | $('#content .albumart').attr("src", "imgs/pandora.png"); 94 | } 95 | }; 96 | 97 | function explainSong() 98 | { 99 | details = $('p.details').html(); 100 | if (details == "EMPTY") 101 | { 102 | $('p.details').html("Grabbing explanation...").fadeToggle('slow'); 103 | $.get("api", {json:JSON.stringify({"method":"GetExplanation", "id":1})}).done(function(explain) 104 | { 105 | $('p.details').fadeOut('slow', function() 106 | { 107 | $(this).html(explain.explanation).fadeIn('slow'); 108 | }); 109 | }); 110 | } 111 | else 112 | $('p.details').fadeToggle('slow'); 113 | } 114 | 115 | function stationSetup() 116 | { 117 | var index = 0; 118 | getStations(index); 119 | Mousetrap.reset(); 120 | Mousetrap.bind('0', function() { changeStation(index + '0'); }); 121 | Mousetrap.bind('1', function() { changeStation(index + '1'); }); 122 | Mousetrap.bind('2', function() { changeStation(index + '2'); }); 123 | Mousetrap.bind('3', function() { changeStation(index + '3'); }); 124 | Mousetrap.bind('4', function() { changeStation(index + '4'); }); 125 | Mousetrap.bind('5', function() { changeStation(index + '5'); }); 126 | Mousetrap.bind('6', function() { changeStation(index + '6'); }); 127 | Mousetrap.bind('7', function() { changeStation(index + '7'); }); 128 | Mousetrap.bind('8', function() { changeStation(index + '8'); }); 129 | Mousetrap.bind('9', function() { changeStation(index + '9'); }); 130 | Mousetrap.bind('n', function() { getStations(++index); }); 131 | Mousetrap.bind('b', function() { getStations(--index); }); 132 | Mousetrap.bind('esc', function() 133 | { 134 | clearScreen('#content', function() 135 | { 136 | setMousetraps(); 137 | }); 138 | }); 139 | } 140 | 141 | function getStations(index) 142 | { 143 | $.get("api", {json:JSON.stringify({"method":"GetStationData", "id":1, "index":index})}).done(function(stationList) 144 | { 145 | stationList = stationList.stationData 146 | clearScreen('#stationList', function() 147 | { 148 | var htmlStationList = "" 149 | if(stationList.back != null) 150 | htmlStationList = "B - Back
\n"; 151 | for(var i = 0; i < stationList.stations.length; i++) 152 | { 153 | htmlStationList += "" + i + " - " + stationList.stations[i] + "
\n" 154 | } 155 | if(stationList.next != null) 156 | htmlStationList += "N - Next
\n"; 157 | $('#stationList').html(htmlStationList); 158 | }); 159 | }); 160 | } 161 | 162 | function changeStation(id) 163 | { 164 | $.get("api", {json:JSON.stringify({"method":"ChangeStation", "id":1, "stationID":id})}) 165 | } 166 | 167 | function newStationSetup() 168 | { 169 | Mousetrap.reset(); 170 | Mousetrap.bind('s', function() { newStation('song'); }); 171 | Mousetrap.bind('a', function() { newStation('artist'); }); 172 | Mousetrap.bind('esc', function() 173 | { 174 | clearScreen('#content', function() 175 | { 176 | setMousetraps(); 177 | }); 178 | }); 179 | clearScreen('#newStation'); 180 | } 181 | 182 | function newStation(type) 183 | { 184 | $.get("api", {json:JSON.stringify({"method":"CreateStation", "id":1, "quick":type})}); 185 | clearScreen('#content', function() 186 | { 187 | setMousetraps(); 188 | }); 189 | } 190 | 191 | function sendCommand(action) 192 | { 193 | $.get("api", {json:JSON.stringify({"method":"Control", "id":1, "command":action})}); 194 | }; 195 | 196 | function pianobarStart() 197 | { 198 | $.get("api", {json:JSON.stringify({"method":"Pianobar.Start", "id":1})}); 199 | }; 200 | function pianobarQuit() 201 | { 202 | $.get("api", {json:JSON.stringify({"method":"Pianobar.Quit", "id":1})}); 203 | }; 204 | 205 | function setMousetraps() 206 | { 207 | Mousetrap.reset(); 208 | Mousetrap.bind(['p', 'space'], function() { sendCommand('pause'); }); 209 | Mousetrap.bind('n', function() { sendCommand('next'); }); 210 | Mousetrap.bind('l', function() { sendCommand('love'); }); 211 | Mousetrap.bind('b', function() { sendCommand('ban'); }); 212 | Mousetrap.bind('t', function() { sendCommand('tired'); }); 213 | Mousetrap.bind('q', function() { pianobarQuit(); }); 214 | Mousetrap.bind('e', function() { explainSong(); }); 215 | Mousetrap.bind('s', function() { stationSetup(); }); 216 | Mousetrap.bind(['(', '-', '_'], function() { sendCommand('volumedown'); }); 217 | Mousetrap.bind([')', '=', '+'], function() { sendCommand('volumeup'); }); 218 | //Mousetrap.bind('c', function() { newStationSetup(); }); See issue#23 219 | } -------------------------------------------------------------------------------- /pidora.py: -------------------------------------------------------------------------------- 1 | import json as libjson, os, re, requests, subprocess 2 | 3 | current_dir = os.path.dirname(os.path.abspath(__file__)) + "/" 4 | 5 | def process(command, new = False): 6 | if new: 7 | with open(os.devnull, "w") as fnull: result = subprocess.Popen(command, stdout = fnull, stderr = fnull) 8 | else: 9 | with open(os.devnull, "w") as fnull: result = subprocess.call(command, stdout = fnull, stderr = fnull) 10 | return result 11 | 12 | def getSongData(data): 13 | if data["songData"] is not None: 14 | jsonObj = data["songData"] 15 | if jsonObj["title"].find("NPR News") != -1: 16 | jsonObj["isSong"] = False 17 | else: 18 | jsonObj["isSong"] = True 19 | return jsonObj 20 | elif "pianobar" in data: 21 | if data["pianobar"] is not None: 22 | return libjson.loads('{"startup":true, "isSong":false}') 23 | else: 24 | return libjson.loads('{"startup":false, "isSong":false}') 25 | else: 26 | return False 27 | 28 | def setSongData(data=dict(), newSongData=dict()): 29 | data['songData'] = newSongData 30 | return True 31 | 32 | def writeMsg(msg): 33 | open(current_dir + "msg", "w").write(msg) 34 | 35 | def getExplanation(data): 36 | songData = getSongData(data) 37 | regex = re.compile("Track(.*?)",re.IGNORECASE|re.DOTALL) 38 | html = requests.get(songData["explainURL"]).text 39 | string = html.replace("\t", "").replace("\n", "").replace('
', "") 40 | r = regex.search(string) 41 | if r is None: 42 | return "We were unable to get the song's explanation. Sorry about that." 43 | data = r.groups()[0].split("
") 44 | traits = data[0:len(data)-1] 45 | if data[len(data)-2].find("many other comedic similarities") == -1: 46 | ending = "many other similarities as identified by the Music Genome Project" 47 | else: 48 | ending = "many other comedic similarities" 49 | traits = data[0:len(data)-2] 50 | return "We're playing this track because it features " + ", ".join(traits) + ", and " + ending +"." 51 | 52 | def getStations(index): 53 | listStations = open(current_dir + "stationList").read().split("|") 54 | stationList = dict(index=index) 55 | lo = index*10 56 | if lo > len(listStations): 57 | return dict(error="No stations in that range") 58 | if len(listStations) < lo+10: 59 | hi = len(listStations) 60 | else: 61 | hi = lo+10 62 | 63 | stationList["back"] = index-1 if lo > 0 else None 64 | stationList["next"] = index+1 if len(listStations) > hi else None 65 | stations = [] 66 | for i in range(lo,hi): 67 | station = listStations[i].split("=") 68 | stations.append(station[1]) 69 | stationList["stations"] = stations 70 | 71 | return stationList 72 | 73 | def Control(command): 74 | commands = dict(pause="p", next="n", love="+", ban="-", tired="t", volumedown="(", volumeup=")") 75 | try: 76 | open(current_dir + "ctl", "w").write(commands[command]) 77 | if command == "next": 78 | writeMsg("Skipped") 79 | elif command == "love": 80 | writeMsg("Loved") 81 | elif command == "ban": 82 | writeMsg("Banned") 83 | elif command == "tired": 84 | writeMsg("Tired") 85 | return True 86 | except KeyError: 87 | return False 88 | 89 | def ChangeStation(id): 90 | open(current_dir + "ctl", "w").write("s" + str(int(id)) + "\n") 91 | writeMsg("Changed station") 92 | return True 93 | 94 | def CreateStation(type, meta): 95 | if type == "quick": 96 | if meta == "song": 97 | open(current_dir + "ctl", "w").write("vs\n") 98 | writeMsg("Station created") 99 | return True 100 | elif meta == "artist": 101 | open(current_dir + "ctl", "w").write("va\n") 102 | writeMsg("Station created") 103 | return True 104 | else: 105 | return False 106 | else: 107 | return False 108 | 109 | def api(data, json=None): 110 | if json is None or json == "": 111 | replyJSON = libjson.dumps(dict(method="NoJSON", id=None, response="bad"), indent=2) 112 | json = libjson.loads(json) 113 | 114 | if json["method"] == "GetSongInfo": 115 | if os.path.exists(current_dir + "msg"): 116 | msg = open(current_dir + "msg").read() 117 | os.remove(current_dir + "msg") 118 | else: 119 | msg = None 120 | songData = getSongData(data) 121 | replyJSON = libjson.dumps(dict(method="GetSongInfo", msg=msg, id=json["id"], song=songData), indent=2) 122 | elif json["method"] == "SetSongInfo": 123 | if 'songData' in json: 124 | if setSongData(data, json["songData"]): 125 | replyJSON = libjson.dumps(dict(method="SetSongInfo", id=json["id"], response="ok")) 126 | else: 127 | replyJSON = libjson.dumps(dict(method="SetSongInfo", id=json["id"], response="bad")) 128 | elif json["method"] == "GetExplanation": 129 | replyJSON = libjson.dumps(dict(method="GetExplanation", id=json["id"], explanation=getExplanation(data)), indent=2) 130 | elif json["method"] == "GetStationData": 131 | replyJSON = libjson.dumps(dict(method="GetStationList", id=json["id"], stationData=getStations(json["index"])), indent=2) 132 | elif json["method"] == "Control": 133 | if Control(json["command"]): 134 | replyJSON = libjson.dumps(dict(method="Control", id=json["id"], command=json["command"], response="ok"), indent=2) 135 | else: 136 | replyJSON = libjson.dumps(dict(method="Control", id=json["id"], command=json["command"], response="bad"), indent=2) 137 | elif json["method"] == "CreateStation": 138 | replyJSON = libjson.dumps(dict(method="CreateStation", id=json["id"], response="disabled - See issue #23"), indent=2) # see issue#23 139 | if json["quick"]: 140 | if CreateStation("quick", json["quick"]): 141 | replyJSON = libjson.dumps(dict(method="CreateStation", id=json["id"], quick=json["quick"], response="ok"), indent=2) 142 | else: 143 | replyJSON = libjson.dumps(dict(method="CreateStation", id=json["id"], quick=json["quick"], response="bad"), indent=2) 144 | else: 145 | replyJSON = libjson.dumps(dict(method="CreateStation", id=json["id"], response="bad"), indent=2) 146 | elif json["method"] == "ChangeStation": 147 | if json["stationID"]: 148 | if ChangeStation(json["stationID"]): 149 | replyJSON = libjson.dumps(dict(method="ChangeStation", id=json["id"], stationID=json["stationID"], response="ok"), indent=2) 150 | else: 151 | replyJSON = libjson.dumps(dict(method="ChangeStation", id=json["id"], stationID=json["stationID"], response="bad"), indent=2) 152 | else: 153 | replyJSON = libjson.dumps(dict(method="ChangeStation", id=json["id"], response="bad"), indent=2) 154 | elif json["method"] == "Pianobar.Start": 155 | if(data["pianobar"] is None): 156 | data["pianobar"] = process(["pianobar"], True) 157 | replyJSON = json=libjson.dumps(dict(method="Pianobar.Start", id=json["id"], response="ok"), indent=2) 158 | else: 159 | replyJSON = json=libjson.dumps(dict(method="Pianobar.Start", id=json["id"], response="bad"), indent=2) 160 | elif json["method"] == "Pianobar.Quit": 161 | if(data["pianobar"]): 162 | open(current_dir + "ctl", "w").write("q") 163 | writeMsg("Shutdown") 164 | os.remove(current_dir + "stationList") 165 | data["songData"] = None 166 | data["pianobar"].wait() 167 | data["pianobar"] = None 168 | replyJSON = libjson.dumps(dict(method="Pianobar.Quit", id=json["id"], response="ok"), indent=2) 169 | else: 170 | replyJSON = libjson.dumps(dict(method="Pianobar.Quit", id=json["id"], response="bad"), indent=2) 171 | else: 172 | replyJSON = libjson.dumps(dict(method="NoValidMethod", id=json["id"], response="bad"), indent=2) 173 | 174 | return dict(data=data, json=replyJSON) -------------------------------------------------------------------------------- /cherrypy/lib/profiler.py: -------------------------------------------------------------------------------- 1 | """Profiler tools for CherryPy. 2 | 3 | CherryPy users 4 | ============== 5 | 6 | You can profile any of your pages as follows:: 7 | 8 | from cherrypy.lib import profiler 9 | 10 | class Root: 11 | p = profile.Profiler("/path/to/profile/dir") 12 | 13 | def index(self): 14 | self.p.run(self._index) 15 | index.exposed = True 16 | 17 | def _index(self): 18 | return "Hello, world!" 19 | 20 | cherrypy.tree.mount(Root()) 21 | 22 | You can also turn on profiling for all requests 23 | using the ``make_app`` function as WSGI middleware. 24 | 25 | CherryPy developers 26 | =================== 27 | 28 | This module can be used whenever you make changes to CherryPy, 29 | to get a quick sanity-check on overall CP performance. Use the 30 | ``--profile`` flag when running the test suite. Then, use the ``serve()`` 31 | function to browse the results in a web browser. If you run this 32 | module from the command line, it will call ``serve()`` for you. 33 | 34 | """ 35 | 36 | 37 | def new_func_strip_path(func_name): 38 | """Make profiler output more readable by adding ``__init__`` modules' parents""" 39 | filename, line, name = func_name 40 | if filename.endswith("__init__.py"): 41 | return os.path.basename(filename[:-12]) + filename[-12:], line, name 42 | return os.path.basename(filename), line, name 43 | 44 | try: 45 | import profile 46 | import pstats 47 | pstats.func_strip_path = new_func_strip_path 48 | except ImportError: 49 | profile = None 50 | pstats = None 51 | 52 | import os, os.path 53 | import sys 54 | import warnings 55 | 56 | from cherrypy._cpcompat import BytesIO 57 | 58 | _count = 0 59 | 60 | class Profiler(object): 61 | 62 | def __init__(self, path=None): 63 | if not path: 64 | path = os.path.join(os.path.dirname(__file__), "profile") 65 | self.path = path 66 | if not os.path.exists(path): 67 | os.makedirs(path) 68 | 69 | def run(self, func, *args, **params): 70 | """Dump profile data into self.path.""" 71 | global _count 72 | c = _count = _count + 1 73 | path = os.path.join(self.path, "cp_%04d.prof" % c) 74 | prof = profile.Profile() 75 | result = prof.runcall(func, *args, **params) 76 | prof.dump_stats(path) 77 | return result 78 | 79 | def statfiles(self): 80 | """:rtype: list of available profiles. 81 | """ 82 | return [f for f in os.listdir(self.path) 83 | if f.startswith("cp_") and f.endswith(".prof")] 84 | 85 | def stats(self, filename, sortby='cumulative'): 86 | """:rtype stats(index): output of print_stats() for the given profile. 87 | """ 88 | sio = BytesIO() 89 | if sys.version_info >= (2, 5): 90 | s = pstats.Stats(os.path.join(self.path, filename), stream=sio) 91 | s.strip_dirs() 92 | s.sort_stats(sortby) 93 | s.print_stats() 94 | else: 95 | # pstats.Stats before Python 2.5 didn't take a 'stream' arg, 96 | # but just printed to stdout. So re-route stdout. 97 | s = pstats.Stats(os.path.join(self.path, filename)) 98 | s.strip_dirs() 99 | s.sort_stats(sortby) 100 | oldout = sys.stdout 101 | try: 102 | sys.stdout = sio 103 | s.print_stats() 104 | finally: 105 | sys.stdout = oldout 106 | response = sio.getvalue() 107 | sio.close() 108 | return response 109 | 110 | def index(self): 111 | return """ 112 | CherryPy profile data 113 | 114 | 115 | 116 | 117 | 118 | """ 119 | index.exposed = True 120 | 121 | def menu(self): 122 | yield "

Profiling runs

" 123 | yield "

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

" 124 | runs = self.statfiles() 125 | runs.sort() 126 | for i in runs: 127 | yield "%s
" % (i, i) 128 | menu.exposed = True 129 | 130 | def report(self, filename): 131 | import cherrypy 132 | cherrypy.response.headers['Content-Type'] = 'text/plain' 133 | return self.stats(filename) 134 | report.exposed = True 135 | 136 | 137 | class ProfileAggregator(Profiler): 138 | 139 | def __init__(self, path=None): 140 | Profiler.__init__(self, path) 141 | global _count 142 | self.count = _count = _count + 1 143 | self.profiler = profile.Profile() 144 | 145 | def run(self, func, *args): 146 | path = os.path.join(self.path, "cp_%04d.prof" % self.count) 147 | result = self.profiler.runcall(func, *args) 148 | self.profiler.dump_stats(path) 149 | return result 150 | 151 | 152 | class make_app: 153 | def __init__(self, nextapp, path=None, aggregate=False): 154 | """Make a WSGI middleware app which wraps 'nextapp' with profiling. 155 | 156 | nextapp 157 | the WSGI application to wrap, usually an instance of 158 | cherrypy.Application. 159 | 160 | path 161 | where to dump the profiling output. 162 | 163 | aggregate 164 | if True, profile data for all HTTP requests will go in 165 | a single file. If False (the default), each HTTP request will 166 | dump its profile data into a separate file. 167 | 168 | """ 169 | if profile is None or pstats is None: 170 | msg = ("Your installation of Python does not have a profile module. " 171 | "If you're on Debian, try `sudo apt-get install python-profiler`. " 172 | "See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.") 173 | warnings.warn(msg) 174 | 175 | self.nextapp = nextapp 176 | self.aggregate = aggregate 177 | if aggregate: 178 | self.profiler = ProfileAggregator(path) 179 | else: 180 | self.profiler = Profiler(path) 181 | 182 | def __call__(self, environ, start_response): 183 | def gather(): 184 | result = [] 185 | for line in self.nextapp(environ, start_response): 186 | result.append(line) 187 | return result 188 | return self.profiler.run(gather) 189 | 190 | 191 | def serve(path=None, port=8080): 192 | if profile is None or pstats is None: 193 | msg = ("Your installation of Python does not have a profile module. " 194 | "If you're on Debian, try `sudo apt-get install python-profiler`. " 195 | "See http://www.cherrypy.org/wiki/ProfilingOnDebian for details.") 196 | warnings.warn(msg) 197 | 198 | import cherrypy 199 | cherrypy.config.update({'server.socket_port': int(port), 200 | 'server.thread_pool': 10, 201 | 'environment': "production", 202 | }) 203 | cherrypy.quickstart(Profiler(path)) 204 | 205 | 206 | if __name__ == "__main__": 207 | serve(*tuple(sys.argv[1:])) 208 | 209 | -------------------------------------------------------------------------------- /cherrypy/_cpthreadinglocal.py: -------------------------------------------------------------------------------- 1 | # This is a backport of Python-2.4's threading.local() implementation 2 | 3 | """Thread-local objects 4 | 5 | (Note that this module provides a Python version of thread 6 | threading.local class. Depending on the version of Python you're 7 | using, there may be a faster one available. You should always import 8 | the local class from threading.) 9 | 10 | Thread-local objects support the management of thread-local data. 11 | If you have data that you want to be local to a thread, simply create 12 | a thread-local object and use its attributes: 13 | 14 | >>> mydata = local() 15 | >>> mydata.number = 42 16 | >>> mydata.number 17 | 42 18 | 19 | You can also access the local-object's dictionary: 20 | 21 | >>> mydata.__dict__ 22 | {'number': 42} 23 | >>> mydata.__dict__.setdefault('widgets', []) 24 | [] 25 | >>> mydata.widgets 26 | [] 27 | 28 | What's important about thread-local objects is that their data are 29 | local to a thread. If we access the data in a different thread: 30 | 31 | >>> log = [] 32 | >>> def f(): 33 | ... items = mydata.__dict__.items() 34 | ... items.sort() 35 | ... log.append(items) 36 | ... mydata.number = 11 37 | ... log.append(mydata.number) 38 | 39 | >>> import threading 40 | >>> thread = threading.Thread(target=f) 41 | >>> thread.start() 42 | >>> thread.join() 43 | >>> log 44 | [[], 11] 45 | 46 | we get different data. Furthermore, changes made in the other thread 47 | don't affect data seen in this thread: 48 | 49 | >>> mydata.number 50 | 42 51 | 52 | Of course, values you get from a local object, including a __dict__ 53 | attribute, are for whatever thread was current at the time the 54 | attribute was read. For that reason, you generally don't want to save 55 | these values across threads, as they apply only to the thread they 56 | came from. 57 | 58 | You can create custom local objects by subclassing the local class: 59 | 60 | >>> class MyLocal(local): 61 | ... number = 2 62 | ... initialized = False 63 | ... def __init__(self, **kw): 64 | ... if self.initialized: 65 | ... raise SystemError('__init__ called too many times') 66 | ... self.initialized = True 67 | ... self.__dict__.update(kw) 68 | ... def squared(self): 69 | ... return self.number ** 2 70 | 71 | This can be useful to support default values, methods and 72 | initialization. Note that if you define an __init__ method, it will be 73 | called each time the local object is used in a separate thread. This 74 | is necessary to initialize each thread's dictionary. 75 | 76 | Now if we create a local object: 77 | 78 | >>> mydata = MyLocal(color='red') 79 | 80 | Now we have a default number: 81 | 82 | >>> mydata.number 83 | 2 84 | 85 | an initial color: 86 | 87 | >>> mydata.color 88 | 'red' 89 | >>> del mydata.color 90 | 91 | And a method that operates on the data: 92 | 93 | >>> mydata.squared() 94 | 4 95 | 96 | As before, we can access the data in a separate thread: 97 | 98 | >>> log = [] 99 | >>> thread = threading.Thread(target=f) 100 | >>> thread.start() 101 | >>> thread.join() 102 | >>> log 103 | [[('color', 'red'), ('initialized', True)], 11] 104 | 105 | without affecting this thread's data: 106 | 107 | >>> mydata.number 108 | 2 109 | >>> mydata.color 110 | Traceback (most recent call last): 111 | ... 112 | AttributeError: 'MyLocal' object has no attribute 'color' 113 | 114 | Note that subclasses can define slots, but they are not thread 115 | local. They are shared across threads: 116 | 117 | >>> class MyLocal(local): 118 | ... __slots__ = 'number' 119 | 120 | >>> mydata = MyLocal() 121 | >>> mydata.number = 42 122 | >>> mydata.color = 'red' 123 | 124 | So, the separate thread: 125 | 126 | >>> thread = threading.Thread(target=f) 127 | >>> thread.start() 128 | >>> thread.join() 129 | 130 | affects what we see: 131 | 132 | >>> mydata.number 133 | 11 134 | 135 | >>> del mydata 136 | """ 137 | 138 | # Threading import is at end 139 | 140 | class _localbase(object): 141 | __slots__ = '_local__key', '_local__args', '_local__lock' 142 | 143 | def __new__(cls, *args, **kw): 144 | self = object.__new__(cls) 145 | key = 'thread.local.' + str(id(self)) 146 | object.__setattr__(self, '_local__key', key) 147 | object.__setattr__(self, '_local__args', (args, kw)) 148 | object.__setattr__(self, '_local__lock', RLock()) 149 | 150 | if args or kw and (cls.__init__ is object.__init__): 151 | raise TypeError("Initialization arguments are not supported") 152 | 153 | # We need to create the thread dict in anticipation of 154 | # __init__ being called, to make sure we don't call it 155 | # again ourselves. 156 | dict = object.__getattribute__(self, '__dict__') 157 | currentThread().__dict__[key] = dict 158 | 159 | return self 160 | 161 | def _patch(self): 162 | key = object.__getattribute__(self, '_local__key') 163 | d = currentThread().__dict__.get(key) 164 | if d is None: 165 | d = {} 166 | currentThread().__dict__[key] = d 167 | object.__setattr__(self, '__dict__', d) 168 | 169 | # we have a new instance dict, so call out __init__ if we have 170 | # one 171 | cls = type(self) 172 | if cls.__init__ is not object.__init__: 173 | args, kw = object.__getattribute__(self, '_local__args') 174 | cls.__init__(self, *args, **kw) 175 | else: 176 | object.__setattr__(self, '__dict__', d) 177 | 178 | class local(_localbase): 179 | 180 | def __getattribute__(self, name): 181 | lock = object.__getattribute__(self, '_local__lock') 182 | lock.acquire() 183 | try: 184 | _patch(self) 185 | return object.__getattribute__(self, name) 186 | finally: 187 | lock.release() 188 | 189 | def __setattr__(self, name, value): 190 | lock = object.__getattribute__(self, '_local__lock') 191 | lock.acquire() 192 | try: 193 | _patch(self) 194 | return object.__setattr__(self, name, value) 195 | finally: 196 | lock.release() 197 | 198 | def __delattr__(self, name): 199 | lock = object.__getattribute__(self, '_local__lock') 200 | lock.acquire() 201 | try: 202 | _patch(self) 203 | return object.__delattr__(self, name) 204 | finally: 205 | lock.release() 206 | 207 | 208 | def __del__(): 209 | threading_enumerate = enumerate 210 | __getattribute__ = object.__getattribute__ 211 | 212 | def __del__(self): 213 | key = __getattribute__(self, '_local__key') 214 | 215 | try: 216 | threads = list(threading_enumerate()) 217 | except: 218 | # if enumerate fails, as it seems to do during 219 | # shutdown, we'll skip cleanup under the assumption 220 | # that there is nothing to clean up 221 | return 222 | 223 | for thread in threads: 224 | try: 225 | __dict__ = thread.__dict__ 226 | except AttributeError: 227 | # Thread is dying, rest in peace 228 | continue 229 | 230 | if key in __dict__: 231 | try: 232 | del __dict__[key] 233 | except KeyError: 234 | pass # didn't have anything in this thread 235 | 236 | return __del__ 237 | __del__ = __del__() 238 | 239 | from threading import currentThread, enumerate, RLock 240 | -------------------------------------------------------------------------------- /cherrypy/lib/gctools.py: -------------------------------------------------------------------------------- 1 | import gc 2 | import inspect 3 | import os 4 | import sys 5 | import time 6 | 7 | try: 8 | import objgraph 9 | except ImportError: 10 | objgraph = None 11 | 12 | import cherrypy 13 | from cherrypy import _cprequest, _cpwsgi 14 | from cherrypy.process.plugins import SimplePlugin 15 | 16 | 17 | class ReferrerTree(object): 18 | """An object which gathers all referrers of an object to a given depth.""" 19 | 20 | peek_length = 40 21 | 22 | def __init__(self, ignore=None, maxdepth=2, maxparents=10): 23 | self.ignore = ignore or [] 24 | self.ignore.append(inspect.currentframe().f_back) 25 | self.maxdepth = maxdepth 26 | self.maxparents = maxparents 27 | 28 | def ascend(self, obj, depth=1): 29 | """Return a nested list containing referrers of the given object.""" 30 | depth += 1 31 | parents = [] 32 | 33 | # Gather all referrers in one step to minimize 34 | # cascading references due to repr() logic. 35 | refs = gc.get_referrers(obj) 36 | self.ignore.append(refs) 37 | if len(refs) > self.maxparents: 38 | return [("[%s referrers]" % len(refs), [])] 39 | 40 | try: 41 | ascendcode = self.ascend.__code__ 42 | except AttributeError: 43 | ascendcode = self.ascend.im_func.func_code 44 | for parent in refs: 45 | if inspect.isframe(parent) and parent.f_code is ascendcode: 46 | continue 47 | if parent in self.ignore: 48 | continue 49 | if depth <= self.maxdepth: 50 | parents.append((parent, self.ascend(parent, depth))) 51 | else: 52 | parents.append((parent, [])) 53 | 54 | return parents 55 | 56 | def peek(self, s): 57 | """Return s, restricted to a sane length.""" 58 | if len(s) > (self.peek_length + 3): 59 | half = self.peek_length // 2 60 | return s[:half] + '...' + s[-half:] 61 | else: 62 | return s 63 | 64 | def _format(self, obj, descend=True): 65 | """Return a string representation of a single object.""" 66 | if inspect.isframe(obj): 67 | filename, lineno, func, context, index = inspect.getframeinfo(obj) 68 | return "" % func 69 | 70 | if not descend: 71 | return self.peek(repr(obj)) 72 | 73 | if isinstance(obj, dict): 74 | return "{" + ", ".join(["%s: %s" % (self._format(k, descend=False), 75 | self._format(v, descend=False)) 76 | for k, v in obj.items()]) + "}" 77 | elif isinstance(obj, list): 78 | return "[" + ", ".join([self._format(item, descend=False) 79 | for item in obj]) + "]" 80 | elif isinstance(obj, tuple): 81 | return "(" + ", ".join([self._format(item, descend=False) 82 | for item in obj]) + ")" 83 | 84 | r = self.peek(repr(obj)) 85 | if isinstance(obj, (str, int, float)): 86 | return r 87 | return "%s: %s" % (type(obj), r) 88 | 89 | def format(self, tree): 90 | """Return a list of string reprs from a nested list of referrers.""" 91 | output = [] 92 | def ascend(branch, depth=1): 93 | for parent, grandparents in branch: 94 | output.append((" " * depth) + self._format(parent)) 95 | if grandparents: 96 | ascend(grandparents, depth + 1) 97 | ascend(tree) 98 | return output 99 | 100 | 101 | def get_instances(cls): 102 | return [x for x in gc.get_objects() if isinstance(x, cls)] 103 | 104 | 105 | class RequestCounter(SimplePlugin): 106 | 107 | def start(self): 108 | self.count = 0 109 | 110 | def before_request(self): 111 | self.count += 1 112 | 113 | def after_request(self): 114 | self.count -=1 115 | request_counter = RequestCounter(cherrypy.engine) 116 | request_counter.subscribe() 117 | 118 | 119 | def get_context(obj): 120 | if isinstance(obj, _cprequest.Request): 121 | return "path=%s;stage=%s" % (obj.path_info, obj.stage) 122 | elif isinstance(obj, _cprequest.Response): 123 | return "status=%s" % obj.status 124 | elif isinstance(obj, _cpwsgi.AppResponse): 125 | return "PATH_INFO=%s" % obj.environ.get('PATH_INFO', '') 126 | elif hasattr(obj, "tb_lineno"): 127 | return "tb_lineno=%s" % obj.tb_lineno 128 | return "" 129 | 130 | 131 | class GCRoot(object): 132 | """A CherryPy page handler for testing reference leaks.""" 133 | 134 | classes = [(_cprequest.Request, 2, 2, 135 | "Should be 1 in this request thread and 1 in the main thread."), 136 | (_cprequest.Response, 2, 2, 137 | "Should be 1 in this request thread and 1 in the main thread."), 138 | (_cpwsgi.AppResponse, 1, 1, 139 | "Should be 1 in this request thread only."), 140 | ] 141 | 142 | def index(self): 143 | return "Hello, world!" 144 | index.exposed = True 145 | 146 | def stats(self): 147 | output = ["Statistics:"] 148 | 149 | for trial in range(10): 150 | if request_counter.count > 0: 151 | break 152 | time.sleep(0.5) 153 | else: 154 | output.append("\nNot all requests closed properly.") 155 | 156 | # gc_collect isn't perfectly synchronous, because it may 157 | # break reference cycles that then take time to fully 158 | # finalize. Call it thrice and hope for the best. 159 | gc.collect() 160 | gc.collect() 161 | unreachable = gc.collect() 162 | if unreachable: 163 | if objgraph is not None: 164 | final = objgraph.by_type('Nondestructible') 165 | if final: 166 | objgraph.show_backrefs(final, filename='finalizers.png') 167 | 168 | trash = {} 169 | for x in gc.garbage: 170 | trash[type(x)] = trash.get(type(x), 0) + 1 171 | if trash: 172 | output.insert(0, "\n%s unreachable objects:" % unreachable) 173 | trash = [(v, k) for k, v in trash.items()] 174 | trash.sort() 175 | for pair in trash: 176 | output.append(" " + repr(pair)) 177 | 178 | # Check declared classes to verify uncollected instances. 179 | # These don't have to be part of a cycle; they can be 180 | # any objects that have unanticipated referrers that keep 181 | # them from being collected. 182 | allobjs = {} 183 | for cls, minobj, maxobj, msg in self.classes: 184 | allobjs[cls] = get_instances(cls) 185 | 186 | for cls, minobj, maxobj, msg in self.classes: 187 | objs = allobjs[cls] 188 | lenobj = len(objs) 189 | if lenobj < minobj or lenobj > maxobj: 190 | if minobj == maxobj: 191 | output.append( 192 | "\nExpected %s %r references, got %s." % 193 | (minobj, cls, lenobj)) 194 | else: 195 | output.append( 196 | "\nExpected %s to %s %r references, got %s." % 197 | (minobj, maxobj, cls, lenobj)) 198 | 199 | for obj in objs: 200 | if objgraph is not None: 201 | ig = [id(objs), id(inspect.currentframe())] 202 | fname = "graph_%s_%s.png" % (cls.__name__, id(obj)) 203 | objgraph.show_backrefs( 204 | obj, extra_ignore=ig, max_depth=4, too_many=20, 205 | filename=fname, extra_info=get_context) 206 | output.append("\nReferrers for %s (refcount=%s):" % 207 | (repr(obj), sys.getrefcount(obj))) 208 | t = ReferrerTree(ignore=[objs], maxdepth=3) 209 | tree = t.ascend(obj) 210 | output.extend(t.format(tree)) 211 | 212 | return "\n".join(output) 213 | stats.exposed = True 214 | 215 | -------------------------------------------------------------------------------- /cherrypy/_cpserver.py: -------------------------------------------------------------------------------- 1 | """Manage HTTP servers with CherryPy.""" 2 | 3 | import warnings 4 | 5 | import cherrypy 6 | from cherrypy.lib import attributes 7 | from cherrypy._cpcompat import basestring, py3k 8 | 9 | # We import * because we want to export check_port 10 | # et al as attributes of this module. 11 | from cherrypy.process.servers import * 12 | 13 | 14 | class Server(ServerAdapter): 15 | """An adapter for an HTTP server. 16 | 17 | You can set attributes (like socket_host and socket_port) 18 | on *this* object (which is probably cherrypy.server), and call 19 | quickstart. For example:: 20 | 21 | cherrypy.server.socket_port = 80 22 | cherrypy.quickstart() 23 | """ 24 | 25 | socket_port = 8080 26 | """The TCP port on which to listen for connections.""" 27 | 28 | _socket_host = '127.0.0.1' 29 | def _get_socket_host(self): 30 | return self._socket_host 31 | def _set_socket_host(self, value): 32 | if value == '': 33 | raise ValueError("The empty string ('') is not an allowed value. " 34 | "Use '0.0.0.0' instead to listen on all active " 35 | "interfaces (INADDR_ANY).") 36 | self._socket_host = value 37 | socket_host = property(_get_socket_host, _set_socket_host, 38 | doc="""The hostname or IP address on which to listen for connections. 39 | 40 | Host values may be any IPv4 or IPv6 address, or any valid hostname. 41 | The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if 42 | your hosts file prefers IPv6). The string '0.0.0.0' is a special 43 | IPv4 entry meaning "any active interface" (INADDR_ANY), and '::' 44 | is the similar IN6ADDR_ANY for IPv6. The empty string or None are 45 | not allowed.""") 46 | 47 | socket_file = None 48 | """If given, the name of the UNIX socket to use instead of TCP/IP. 49 | 50 | When this option is not None, the `socket_host` and `socket_port` options 51 | are ignored.""" 52 | 53 | socket_queue_size = 5 54 | """The 'backlog' argument to socket.listen(); specifies the maximum number 55 | of queued connections (default 5).""" 56 | 57 | socket_timeout = 10 58 | """The timeout in seconds for accepted connections (default 10).""" 59 | 60 | shutdown_timeout = 5 61 | """The time to wait for HTTP worker threads to clean up.""" 62 | 63 | protocol_version = 'HTTP/1.1' 64 | """The version string to write in the Status-Line of all HTTP responses, 65 | for example, "HTTP/1.1" (the default). Depending on the HTTP server used, 66 | this should also limit the supported features used in the response.""" 67 | 68 | thread_pool = 10 69 | """The number of worker threads to start up in the pool.""" 70 | 71 | thread_pool_max = -1 72 | """The maximum size of the worker-thread pool. Use -1 to indicate no limit.""" 73 | 74 | max_request_header_size = 500 * 1024 75 | """The maximum number of bytes allowable in the request headers. If exceeded, 76 | the HTTP server should return "413 Request Entity Too Large".""" 77 | 78 | max_request_body_size = 100 * 1024 * 1024 79 | """The maximum number of bytes allowable in the request body. If exceeded, 80 | the HTTP server should return "413 Request Entity Too Large".""" 81 | 82 | instance = None 83 | """If not None, this should be an HTTP server instance (such as 84 | CPWSGIServer) which cherrypy.server will control. Use this when you need 85 | more control over object instantiation than is available in the various 86 | configuration options.""" 87 | 88 | ssl_context = None 89 | """When using PyOpenSSL, an instance of SSL.Context.""" 90 | 91 | ssl_certificate = None 92 | """The filename of the SSL certificate to use.""" 93 | 94 | ssl_certificate_chain = None 95 | """When using PyOpenSSL, the certificate chain to pass to 96 | Context.load_verify_locations.""" 97 | 98 | ssl_private_key = None 99 | """The filename of the private key to use with SSL.""" 100 | 101 | if py3k: 102 | ssl_module = 'builtin' 103 | """The name of a registered SSL adaptation module to use with the builtin 104 | WSGI server. Builtin options are: 'builtin' (to use the SSL library built 105 | into recent versions of Python). You may also register your 106 | own classes in the wsgiserver.ssl_adapters dict.""" 107 | else: 108 | ssl_module = 'pyopenssl' 109 | """The name of a registered SSL adaptation module to use with the builtin 110 | WSGI server. Builtin options are 'builtin' (to use the SSL library built 111 | into recent versions of Python) and 'pyopenssl' (to use the PyOpenSSL 112 | project, which you must install separately). You may also register your 113 | own classes in the wsgiserver.ssl_adapters dict.""" 114 | 115 | statistics = False 116 | """Turns statistics-gathering on or off for aware HTTP servers.""" 117 | 118 | nodelay = True 119 | """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" 120 | 121 | wsgi_version = (1, 0) 122 | """The WSGI version tuple to use with the builtin WSGI server. 123 | The provided options are (1, 0) [which includes support for PEP 3333, 124 | which declares it covers WSGI version 1.0.1 but still mandates the 125 | wsgi.version (1, 0)] and ('u', 0), an experimental unicode version. 126 | You may create and register your own experimental versions of the WSGI 127 | protocol by adding custom classes to the wsgiserver.wsgi_gateways dict.""" 128 | 129 | def __init__(self): 130 | self.bus = cherrypy.engine 131 | self.httpserver = None 132 | self.interrupt = None 133 | self.running = False 134 | 135 | def httpserver_from_self(self, httpserver=None): 136 | """Return a (httpserver, bind_addr) pair based on self attributes.""" 137 | if httpserver is None: 138 | httpserver = self.instance 139 | if httpserver is None: 140 | from cherrypy import _cpwsgi_server 141 | httpserver = _cpwsgi_server.CPWSGIServer(self) 142 | if isinstance(httpserver, basestring): 143 | # Is anyone using this? Can I add an arg? 144 | httpserver = attributes(httpserver)(self) 145 | return httpserver, self.bind_addr 146 | 147 | def start(self): 148 | """Start the HTTP server.""" 149 | if not self.httpserver: 150 | self.httpserver, self.bind_addr = self.httpserver_from_self() 151 | ServerAdapter.start(self) 152 | start.priority = 75 153 | 154 | def _get_bind_addr(self): 155 | if self.socket_file: 156 | return self.socket_file 157 | if self.socket_host is None and self.socket_port is None: 158 | return None 159 | return (self.socket_host, self.socket_port) 160 | def _set_bind_addr(self, value): 161 | if value is None: 162 | self.socket_file = None 163 | self.socket_host = None 164 | self.socket_port = None 165 | elif isinstance(value, basestring): 166 | self.socket_file = value 167 | self.socket_host = None 168 | self.socket_port = None 169 | else: 170 | try: 171 | self.socket_host, self.socket_port = value 172 | self.socket_file = None 173 | except ValueError: 174 | raise ValueError("bind_addr must be a (host, port) tuple " 175 | "(for TCP sockets) or a string (for Unix " 176 | "domain sockets), not %r" % value) 177 | bind_addr = property(_get_bind_addr, _set_bind_addr, 178 | doc='A (host, port) tuple for TCP sockets or a str for Unix domain sockets.') 179 | 180 | def base(self): 181 | """Return the base (scheme://host[:port] or sock file) for this server.""" 182 | if self.socket_file: 183 | return self.socket_file 184 | 185 | host = self.socket_host 186 | if host in ('0.0.0.0', '::'): 187 | # 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY. 188 | # Look up the host name, which should be the 189 | # safest thing to spit out in a URL. 190 | import socket 191 | host = socket.gethostname() 192 | 193 | port = self.socket_port 194 | 195 | if self.ssl_certificate: 196 | scheme = "https" 197 | if port != 443: 198 | host += ":%s" % port 199 | else: 200 | scheme = "http" 201 | if port != 80: 202 | host += ":%s" % port 203 | 204 | return "%s://%s" % (scheme, host) 205 | 206 | -------------------------------------------------------------------------------- /cherrypy/wsgiserver/ssl_pyopenssl.py: -------------------------------------------------------------------------------- 1 | """A library for integrating pyOpenSSL with CherryPy. 2 | 3 | The OpenSSL module must be importable for SSL functionality. 4 | You can obtain it from http://pyopenssl.sourceforge.net/ 5 | 6 | To use this module, set CherryPyWSGIServer.ssl_adapter to an instance of 7 | SSLAdapter. There are two ways to use SSL: 8 | 9 | Method One 10 | ---------- 11 | 12 | * ``ssl_adapter.context``: an instance of SSL.Context. 13 | 14 | If this is not None, it is assumed to be an SSL.Context instance, 15 | and will be passed to SSL.Connection on bind(). The developer is 16 | responsible for forming a valid Context object. This approach is 17 | to be preferred for more flexibility, e.g. if the cert and key are 18 | streams instead of files, or need decryption, or SSL.SSLv3_METHOD 19 | is desired instead of the default SSL.SSLv23_METHOD, etc. Consult 20 | the pyOpenSSL documentation for complete options. 21 | 22 | Method Two (shortcut) 23 | --------------------- 24 | 25 | * ``ssl_adapter.certificate``: the filename of the server SSL certificate. 26 | * ``ssl_adapter.private_key``: the filename of the server's private key file. 27 | 28 | Both are None by default. If ssl_adapter.context is None, but .private_key 29 | and .certificate are both given and valid, they will be read, and the 30 | context will be automatically created from them. 31 | """ 32 | 33 | import socket 34 | import threading 35 | import time 36 | 37 | from cherrypy import wsgiserver 38 | 39 | try: 40 | from OpenSSL import SSL 41 | from OpenSSL import crypto 42 | except ImportError: 43 | SSL = None 44 | 45 | 46 | class SSL_fileobject(wsgiserver.CP_fileobject): 47 | """SSL file object attached to a socket object.""" 48 | 49 | ssl_timeout = 3 50 | ssl_retry = .01 51 | 52 | def _safe_call(self, is_reader, call, *args, **kwargs): 53 | """Wrap the given call with SSL error-trapping. 54 | 55 | is_reader: if False EOF errors will be raised. If True, EOF errors 56 | will return "" (to emulate normal sockets). 57 | """ 58 | start = time.time() 59 | while True: 60 | try: 61 | return call(*args, **kwargs) 62 | except SSL.WantReadError: 63 | # Sleep and try again. This is dangerous, because it means 64 | # the rest of the stack has no way of differentiating 65 | # between a "new handshake" error and "client dropped". 66 | # Note this isn't an endless loop: there's a timeout below. 67 | time.sleep(self.ssl_retry) 68 | except SSL.WantWriteError: 69 | time.sleep(self.ssl_retry) 70 | except SSL.SysCallError, e: 71 | if is_reader and e.args == (-1, 'Unexpected EOF'): 72 | return "" 73 | 74 | errnum = e.args[0] 75 | if is_reader and errnum in wsgiserver.socket_errors_to_ignore: 76 | return "" 77 | raise socket.error(errnum) 78 | except SSL.Error, e: 79 | if is_reader and e.args == (-1, 'Unexpected EOF'): 80 | return "" 81 | 82 | thirdarg = None 83 | try: 84 | thirdarg = e.args[0][0][2] 85 | except IndexError: 86 | pass 87 | 88 | if thirdarg == 'http request': 89 | # The client is talking HTTP to an HTTPS server. 90 | raise wsgiserver.NoSSLError() 91 | 92 | raise wsgiserver.FatalSSLAlert(*e.args) 93 | except: 94 | raise 95 | 96 | if time.time() - start > self.ssl_timeout: 97 | raise socket.timeout("timed out") 98 | 99 | def recv(self, *args, **kwargs): 100 | buf = [] 101 | r = super(SSL_fileobject, self).recv 102 | while True: 103 | data = self._safe_call(True, r, *args, **kwargs) 104 | buf.append(data) 105 | p = self._sock.pending() 106 | if not p: 107 | return "".join(buf) 108 | 109 | def sendall(self, *args, **kwargs): 110 | return self._safe_call(False, super(SSL_fileobject, self).sendall, 111 | *args, **kwargs) 112 | 113 | def send(self, *args, **kwargs): 114 | return self._safe_call(False, super(SSL_fileobject, self).send, 115 | *args, **kwargs) 116 | 117 | 118 | class SSLConnection: 119 | """A thread-safe wrapper for an SSL.Connection. 120 | 121 | ``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``. 122 | """ 123 | 124 | def __init__(self, *args): 125 | self._ssl_conn = SSL.Connection(*args) 126 | self._lock = threading.RLock() 127 | 128 | for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read', 129 | 'renegotiate', 'bind', 'listen', 'connect', 'accept', 130 | 'setblocking', 'fileno', 'close', 'get_cipher_list', 131 | 'getpeername', 'getsockname', 'getsockopt', 'setsockopt', 132 | 'makefile', 'get_app_data', 'set_app_data', 'state_string', 133 | 'sock_shutdown', 'get_peer_certificate', 'want_read', 134 | 'want_write', 'set_connect_state', 'set_accept_state', 135 | 'connect_ex', 'sendall', 'settimeout', 'gettimeout'): 136 | exec("""def %s(self, *args): 137 | self._lock.acquire() 138 | try: 139 | return self._ssl_conn.%s(*args) 140 | finally: 141 | self._lock.release() 142 | """ % (f, f)) 143 | 144 | def shutdown(self, *args): 145 | self._lock.acquire() 146 | try: 147 | # pyOpenSSL.socket.shutdown takes no args 148 | return self._ssl_conn.shutdown() 149 | finally: 150 | self._lock.release() 151 | 152 | 153 | class pyOpenSSLAdapter(wsgiserver.SSLAdapter): 154 | """A wrapper for integrating pyOpenSSL with CherryPy.""" 155 | 156 | context = None 157 | """An instance of SSL.Context.""" 158 | 159 | certificate = None 160 | """The filename of the server SSL certificate.""" 161 | 162 | private_key = None 163 | """The filename of the server's private key file.""" 164 | 165 | certificate_chain = None 166 | """Optional. The filename of CA's intermediate certificate bundle. 167 | 168 | This is needed for cheaper "chained root" SSL certificates, and should be 169 | left as None if not required.""" 170 | 171 | def __init__(self, certificate, private_key, certificate_chain=None): 172 | if SSL is None: 173 | raise ImportError("You must install pyOpenSSL to use HTTPS.") 174 | 175 | self.context = None 176 | self.certificate = certificate 177 | self.private_key = private_key 178 | self.certificate_chain = certificate_chain 179 | self._environ = None 180 | 181 | def bind(self, sock): 182 | """Wrap and return the given socket.""" 183 | if self.context is None: 184 | self.context = self.get_context() 185 | conn = SSLConnection(self.context, sock) 186 | self._environ = self.get_environ() 187 | return conn 188 | 189 | def wrap(self, sock): 190 | """Wrap and return the given socket, plus WSGI environ entries.""" 191 | return sock, self._environ.copy() 192 | 193 | def get_context(self): 194 | """Return an SSL.Context from self attributes.""" 195 | # See http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/442473 196 | c = SSL.Context(SSL.SSLv23_METHOD) 197 | c.use_privatekey_file(self.private_key) 198 | if self.certificate_chain: 199 | c.load_verify_locations(self.certificate_chain) 200 | c.use_certificate_file(self.certificate) 201 | return c 202 | 203 | def get_environ(self): 204 | """Return WSGI environ entries to be merged into each request.""" 205 | ssl_environ = { 206 | "HTTPS": "on", 207 | # pyOpenSSL doesn't provide access to any of these AFAICT 208 | ## 'SSL_PROTOCOL': 'SSLv2', 209 | ## SSL_CIPHER string The cipher specification name 210 | ## SSL_VERSION_INTERFACE string The mod_ssl program version 211 | ## SSL_VERSION_LIBRARY string The OpenSSL program version 212 | } 213 | 214 | if self.certificate: 215 | # Server certificate attributes 216 | cert = open(self.certificate, 'rb').read() 217 | cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert) 218 | ssl_environ.update({ 219 | 'SSL_SERVER_M_VERSION': cert.get_version(), 220 | 'SSL_SERVER_M_SERIAL': cert.get_serial_number(), 221 | ## 'SSL_SERVER_V_START': Validity of server's certificate (start time), 222 | ## 'SSL_SERVER_V_END': Validity of server's certificate (end time), 223 | }) 224 | 225 | for prefix, dn in [("I", cert.get_issuer()), 226 | ("S", cert.get_subject())]: 227 | # X509Name objects don't seem to have a way to get the 228 | # complete DN string. Use str() and slice it instead, 229 | # because str(dn) == "" 230 | dnstr = str(dn)[18:-2] 231 | 232 | wsgikey = 'SSL_SERVER_%s_DN' % prefix 233 | ssl_environ[wsgikey] = dnstr 234 | 235 | # The DN should be of the form: /k1=v1/k2=v2, but we must allow 236 | # for any value to contain slashes itself (in a URL). 237 | while dnstr: 238 | pos = dnstr.rfind("=") 239 | dnstr, value = dnstr[:pos], dnstr[pos + 1:] 240 | pos = dnstr.rfind("/") 241 | dnstr, key = dnstr[:pos], dnstr[pos + 1:] 242 | if key and value: 243 | wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key) 244 | ssl_environ[wsgikey] = value 245 | 246 | return ssl_environ 247 | 248 | def makefile(self, sock, mode='r', bufsize=-1): 249 | if SSL and isinstance(sock, SSL.ConnectionType): 250 | timeout = sock.gettimeout() 251 | f = SSL_fileobject(sock, mode, bufsize) 252 | f.ssl_timeout = timeout 253 | return f 254 | else: 255 | return wsgiserver.CP_fileobject(sock, mode, bufsize) 256 | 257 | -------------------------------------------------------------------------------- /cherrypy/_cpconfig.py: -------------------------------------------------------------------------------- 1 | """ 2 | Configuration system for CherryPy. 3 | 4 | Configuration in CherryPy is implemented via dictionaries. Keys are strings 5 | which name the mapped value, which may be of any type. 6 | 7 | 8 | Architecture 9 | ------------ 10 | 11 | CherryPy Requests are part of an Application, which runs in a global context, 12 | and configuration data may apply to any of those three scopes: 13 | 14 | Global 15 | Configuration entries which apply everywhere are stored in 16 | cherrypy.config. 17 | 18 | Application 19 | Entries which apply to each mounted application are stored 20 | on the Application object itself, as 'app.config'. This is a two-level 21 | dict where each key is a path, or "relative URL" (for example, "/" or 22 | "/path/to/my/page"), and each value is a config dict. Usually, this 23 | data is provided in the call to tree.mount(root(), config=conf), 24 | although you may also use app.merge(conf). 25 | 26 | Request 27 | Each Request object possesses a single 'Request.config' dict. 28 | Early in the request process, this dict is populated by merging global 29 | config entries, Application entries (whose path equals or is a parent 30 | of Request.path_info), and any config acquired while looking up the 31 | page handler (see next). 32 | 33 | 34 | Declaration 35 | ----------- 36 | 37 | Configuration data may be supplied as a Python dictionary, as a filename, 38 | or as an open file object. When you supply a filename or file, CherryPy 39 | uses Python's builtin ConfigParser; you declare Application config by 40 | writing each path as a section header:: 41 | 42 | [/path/to/my/page] 43 | request.stream = True 44 | 45 | To declare global configuration entries, place them in a [global] section. 46 | 47 | You may also declare config entries directly on the classes and methods 48 | (page handlers) that make up your CherryPy application via the ``_cp_config`` 49 | attribute. For example:: 50 | 51 | class Demo: 52 | _cp_config = {'tools.gzip.on': True} 53 | 54 | def index(self): 55 | return "Hello world" 56 | index.exposed = True 57 | index._cp_config = {'request.show_tracebacks': False} 58 | 59 | .. note:: 60 | 61 | This behavior is only guaranteed for the default dispatcher. 62 | Other dispatchers may have different restrictions on where 63 | you can attach _cp_config attributes. 64 | 65 | 66 | Namespaces 67 | ---------- 68 | 69 | Configuration keys are separated into namespaces by the first "." in the key. 70 | Current namespaces: 71 | 72 | engine 73 | Controls the 'application engine', including autoreload. 74 | These can only be declared in the global config. 75 | 76 | tree 77 | Grafts cherrypy.Application objects onto cherrypy.tree. 78 | These can only be declared in the global config. 79 | 80 | hooks 81 | Declares additional request-processing functions. 82 | 83 | log 84 | Configures the logging for each application. 85 | These can only be declared in the global or / config. 86 | 87 | request 88 | Adds attributes to each Request. 89 | 90 | response 91 | Adds attributes to each Response. 92 | 93 | server 94 | Controls the default HTTP server via cherrypy.server. 95 | These can only be declared in the global config. 96 | 97 | tools 98 | Runs and configures additional request-processing packages. 99 | 100 | wsgi 101 | Adds WSGI middleware to an Application's "pipeline". 102 | These can only be declared in the app's root config ("/"). 103 | 104 | checker 105 | Controls the 'checker', which looks for common errors in 106 | app state (including config) when the engine starts. 107 | Global config only. 108 | 109 | The only key that does not exist in a namespace is the "environment" entry. 110 | This special entry 'imports' other config entries from a template stored in 111 | cherrypy._cpconfig.environments[environment]. It only applies to the global 112 | config, and only when you use cherrypy.config.update. 113 | 114 | You can define your own namespaces to be called at the Global, Application, 115 | or Request level, by adding a named handler to cherrypy.config.namespaces, 116 | app.namespaces, or app.request_class.namespaces. The name can 117 | be any string, and the handler must be either a callable or a (Python 2.5 118 | style) context manager. 119 | """ 120 | 121 | import cherrypy 122 | from cherrypy._cpcompat import set, basestring 123 | from cherrypy.lib import reprconf 124 | 125 | # Deprecated in CherryPy 3.2--remove in 3.3 126 | NamespaceSet = reprconf.NamespaceSet 127 | 128 | def merge(base, other): 129 | """Merge one app config (from a dict, file, or filename) into another. 130 | 131 | If the given config is a filename, it will be appended to 132 | the list of files to monitor for "autoreload" changes. 133 | """ 134 | if isinstance(other, basestring): 135 | cherrypy.engine.autoreload.files.add(other) 136 | 137 | # Load other into base 138 | for section, value_map in reprconf.as_dict(other).items(): 139 | if not isinstance(value_map, dict): 140 | raise ValueError( 141 | "Application config must include section headers, but the " 142 | "config you tried to merge doesn't have any sections. " 143 | "Wrap your config in another dict with paths as section " 144 | "headers, for example: {'/': config}.") 145 | base.setdefault(section, {}).update(value_map) 146 | 147 | 148 | class Config(reprconf.Config): 149 | """The 'global' configuration data for the entire CherryPy process.""" 150 | 151 | def update(self, config): 152 | """Update self from a dict, file or filename.""" 153 | if isinstance(config, basestring): 154 | # Filename 155 | cherrypy.engine.autoreload.files.add(config) 156 | reprconf.Config.update(self, config) 157 | 158 | def _apply(self, config): 159 | """Update self from a dict.""" 160 | if isinstance(config.get("global", None), dict): 161 | if len(config) > 1: 162 | cherrypy.checker.global_config_contained_paths = True 163 | config = config["global"] 164 | if 'tools.staticdir.dir' in config: 165 | config['tools.staticdir.section'] = "global" 166 | reprconf.Config._apply(self, config) 167 | 168 | def __call__(self, *args, **kwargs): 169 | """Decorator for page handlers to set _cp_config.""" 170 | if args: 171 | raise TypeError( 172 | "The cherrypy.config decorator does not accept positional " 173 | "arguments; you must use keyword arguments.") 174 | def tool_decorator(f): 175 | if not hasattr(f, "_cp_config"): 176 | f._cp_config = {} 177 | for k, v in kwargs.items(): 178 | f._cp_config[k] = v 179 | return f 180 | return tool_decorator 181 | 182 | 183 | Config.environments = environments = { 184 | "staging": { 185 | 'engine.autoreload_on': False, 186 | 'checker.on': False, 187 | 'tools.log_headers.on': False, 188 | 'request.show_tracebacks': False, 189 | 'request.show_mismatched_params': False, 190 | }, 191 | "production": { 192 | 'engine.autoreload_on': False, 193 | 'checker.on': False, 194 | 'tools.log_headers.on': False, 195 | 'request.show_tracebacks': False, 196 | 'request.show_mismatched_params': False, 197 | 'log.screen': False, 198 | }, 199 | "embedded": { 200 | # For use with CherryPy embedded in another deployment stack. 201 | 'engine.autoreload_on': False, 202 | 'checker.on': False, 203 | 'tools.log_headers.on': False, 204 | 'request.show_tracebacks': False, 205 | 'request.show_mismatched_params': False, 206 | 'log.screen': False, 207 | 'engine.SIGHUP': None, 208 | 'engine.SIGTERM': None, 209 | }, 210 | "test_suite": { 211 | 'engine.autoreload_on': False, 212 | 'checker.on': False, 213 | 'tools.log_headers.on': False, 214 | 'request.show_tracebacks': True, 215 | 'request.show_mismatched_params': True, 216 | 'log.screen': False, 217 | }, 218 | } 219 | 220 | 221 | def _server_namespace_handler(k, v): 222 | """Config handler for the "server" namespace.""" 223 | atoms = k.split(".", 1) 224 | if len(atoms) > 1: 225 | # Special-case config keys of the form 'server.servername.socket_port' 226 | # to configure additional HTTP servers. 227 | if not hasattr(cherrypy, "servers"): 228 | cherrypy.servers = {} 229 | 230 | servername, k = atoms 231 | if servername not in cherrypy.servers: 232 | from cherrypy import _cpserver 233 | cherrypy.servers[servername] = _cpserver.Server() 234 | # On by default, but 'on = False' can unsubscribe it (see below). 235 | cherrypy.servers[servername].subscribe() 236 | 237 | if k == 'on': 238 | if v: 239 | cherrypy.servers[servername].subscribe() 240 | else: 241 | cherrypy.servers[servername].unsubscribe() 242 | else: 243 | setattr(cherrypy.servers[servername], k, v) 244 | else: 245 | setattr(cherrypy.server, k, v) 246 | Config.namespaces["server"] = _server_namespace_handler 247 | 248 | def _engine_namespace_handler(k, v): 249 | """Backward compatibility handler for the "engine" namespace.""" 250 | engine = cherrypy.engine 251 | if k == 'autoreload_on': 252 | if v: 253 | engine.autoreload.subscribe() 254 | else: 255 | engine.autoreload.unsubscribe() 256 | elif k == 'autoreload_frequency': 257 | engine.autoreload.frequency = v 258 | elif k == 'autoreload_match': 259 | engine.autoreload.match = v 260 | elif k == 'reload_files': 261 | engine.autoreload.files = set(v) 262 | elif k == 'deadlock_poll_freq': 263 | engine.timeout_monitor.frequency = v 264 | elif k == 'SIGHUP': 265 | engine.listeners['SIGHUP'] = set([v]) 266 | elif k == 'SIGTERM': 267 | engine.listeners['SIGTERM'] = set([v]) 268 | elif "." in k: 269 | plugin, attrname = k.split(".", 1) 270 | plugin = getattr(engine, plugin) 271 | if attrname == 'on': 272 | if v and hasattr(getattr(plugin, 'subscribe', None), '__call__'): 273 | plugin.subscribe() 274 | return 275 | elif (not v) and hasattr(getattr(plugin, 'unsubscribe', None), '__call__'): 276 | plugin.unsubscribe() 277 | return 278 | setattr(plugin, attrname, v) 279 | else: 280 | setattr(engine, k, v) 281 | Config.namespaces["engine"] = _engine_namespace_handler 282 | 283 | 284 | def _tree_namespace_handler(k, v): 285 | """Namespace handler for the 'tree' config namespace.""" 286 | if isinstance(v, dict): 287 | for script_name, app in v.items(): 288 | cherrypy.tree.graft(app, script_name) 289 | cherrypy.engine.log("Mounted: %s on %s" % (app, script_name or "/")) 290 | else: 291 | cherrypy.tree.graft(v, v.script_name) 292 | cherrypy.engine.log("Mounted: %s on %s" % (v, v.script_name or "/")) 293 | Config.namespaces["tree"] = _tree_namespace_handler 294 | 295 | 296 | -------------------------------------------------------------------------------- /cherrypy/_cpcompat.py: -------------------------------------------------------------------------------- 1 | """Compatibility code for using CherryPy with various versions of Python. 2 | 3 | CherryPy 3.2 is compatible with Python versions 2.3+. This module provides a 4 | useful abstraction over the differences between Python versions, sometimes by 5 | preferring a newer idiom, sometimes an older one, and sometimes a custom one. 6 | 7 | In particular, Python 2 uses str and '' for byte strings, while Python 3 8 | uses str and '' for unicode strings. We will call each of these the 'native 9 | string' type for each version. Because of this major difference, this module 10 | provides new 'bytestr', 'unicodestr', and 'nativestr' attributes, as well as 11 | two functions: 'ntob', which translates native strings (of type 'str') into 12 | byte strings regardless of Python version, and 'ntou', which translates native 13 | strings to unicode strings. This also provides a 'BytesIO' name for dealing 14 | specifically with bytes, and a 'StringIO' name for dealing with native strings. 15 | It also provides a 'base64_decode' function with native strings as input and 16 | output. 17 | """ 18 | import os 19 | import re 20 | import sys 21 | 22 | if sys.version_info >= (3, 0): 23 | py3k = True 24 | bytestr = bytes 25 | unicodestr = str 26 | nativestr = unicodestr 27 | basestring = (bytes, str) 28 | def ntob(n, encoding='ISO-8859-1'): 29 | """Return the given native string as a byte string in the given encoding.""" 30 | # In Python 3, the native string type is unicode 31 | return n.encode(encoding) 32 | def ntou(n, encoding='ISO-8859-1'): 33 | """Return the given native string as a unicode string with the given encoding.""" 34 | # In Python 3, the native string type is unicode 35 | return n 36 | def tonative(n, encoding='ISO-8859-1'): 37 | """Return the given string as a native string in the given encoding.""" 38 | # In Python 3, the native string type is unicode 39 | if isinstance(n, bytes): 40 | return n.decode(encoding) 41 | return n 42 | # type("") 43 | from io import StringIO 44 | # bytes: 45 | from io import BytesIO as BytesIO 46 | else: 47 | # Python 2 48 | py3k = False 49 | bytestr = str 50 | unicodestr = unicode 51 | nativestr = bytestr 52 | basestring = basestring 53 | def ntob(n, encoding='ISO-8859-1'): 54 | """Return the given native string as a byte string in the given encoding.""" 55 | # In Python 2, the native string type is bytes. Assume it's already 56 | # in the given encoding, which for ISO-8859-1 is almost always what 57 | # was intended. 58 | return n 59 | def ntou(n, encoding='ISO-8859-1'): 60 | """Return the given native string as a unicode string with the given encoding.""" 61 | # In Python 2, the native string type is bytes. 62 | # First, check for the special encoding 'escape'. The test suite uses this 63 | # to signal that it wants to pass a string with embedded \uXXXX escapes, 64 | # but without having to prefix it with u'' for Python 2, but no prefix 65 | # for Python 3. 66 | if encoding == 'escape': 67 | return unicode( 68 | re.sub(r'\\u([0-9a-zA-Z]{4})', 69 | lambda m: unichr(int(m.group(1), 16)), 70 | n.decode('ISO-8859-1'))) 71 | # Assume it's already in the given encoding, which for ISO-8859-1 is almost 72 | # always what was intended. 73 | return n.decode(encoding) 74 | def tonative(n, encoding='ISO-8859-1'): 75 | """Return the given string as a native string in the given encoding.""" 76 | # In Python 2, the native string type is bytes. 77 | if isinstance(n, unicode): 78 | return n.encode(encoding) 79 | return n 80 | try: 81 | # type("") 82 | from cStringIO import StringIO 83 | except ImportError: 84 | # type("") 85 | from StringIO import StringIO 86 | # bytes: 87 | BytesIO = StringIO 88 | 89 | try: 90 | set = set 91 | except NameError: 92 | from sets import Set as set 93 | 94 | try: 95 | # Python 3.1+ 96 | from base64 import decodebytes as _base64_decodebytes 97 | except ImportError: 98 | # Python 3.0- 99 | # since CherryPy claims compability with Python 2.3, we must use 100 | # the legacy API of base64 101 | from base64 import decodestring as _base64_decodebytes 102 | 103 | def base64_decode(n, encoding='ISO-8859-1'): 104 | """Return the native string base64-decoded (as a native string).""" 105 | if isinstance(n, unicodestr): 106 | b = n.encode(encoding) 107 | else: 108 | b = n 109 | b = _base64_decodebytes(b) 110 | if nativestr is unicodestr: 111 | return b.decode(encoding) 112 | else: 113 | return b 114 | 115 | try: 116 | # Python 2.5+ 117 | from hashlib import md5 118 | except ImportError: 119 | from md5 import new as md5 120 | 121 | try: 122 | # Python 2.5+ 123 | from hashlib import sha1 as sha 124 | except ImportError: 125 | from sha import new as sha 126 | 127 | try: 128 | sorted = sorted 129 | except NameError: 130 | def sorted(i): 131 | i = i[:] 132 | i.sort() 133 | return i 134 | 135 | try: 136 | reversed = reversed 137 | except NameError: 138 | def reversed(x): 139 | i = len(x) 140 | while i > 0: 141 | i -= 1 142 | yield x[i] 143 | 144 | try: 145 | # Python 3 146 | from urllib.parse import urljoin, urlencode 147 | from urllib.parse import quote, quote_plus 148 | from urllib.request import unquote, urlopen 149 | from urllib.request import parse_http_list, parse_keqv_list 150 | except ImportError: 151 | # Python 2 152 | from urlparse import urljoin 153 | from urllib import urlencode, urlopen 154 | from urllib import quote, quote_plus 155 | from urllib import unquote 156 | from urllib2 import parse_http_list, parse_keqv_list 157 | 158 | try: 159 | from threading import local as threadlocal 160 | except ImportError: 161 | from cherrypy._cpthreadinglocal import local as threadlocal 162 | 163 | try: 164 | dict.iteritems 165 | # Python 2 166 | iteritems = lambda d: d.iteritems() 167 | copyitems = lambda d: d.items() 168 | except AttributeError: 169 | # Python 3 170 | iteritems = lambda d: d.items() 171 | copyitems = lambda d: list(d.items()) 172 | 173 | try: 174 | dict.iterkeys 175 | # Python 2 176 | iterkeys = lambda d: d.iterkeys() 177 | copykeys = lambda d: d.keys() 178 | except AttributeError: 179 | # Python 3 180 | iterkeys = lambda d: d.keys() 181 | copykeys = lambda d: list(d.keys()) 182 | 183 | try: 184 | dict.itervalues 185 | # Python 2 186 | itervalues = lambda d: d.itervalues() 187 | copyvalues = lambda d: d.values() 188 | except AttributeError: 189 | # Python 3 190 | itervalues = lambda d: d.values() 191 | copyvalues = lambda d: list(d.values()) 192 | 193 | try: 194 | # Python 3 195 | import builtins 196 | except ImportError: 197 | # Python 2 198 | import __builtin__ as builtins 199 | 200 | try: 201 | # Python 2. We have to do it in this order so Python 2 builds 202 | # don't try to import the 'http' module from cherrypy.lib 203 | from Cookie import SimpleCookie, CookieError 204 | from httplib import BadStatusLine, HTTPConnection, HTTPSConnection, IncompleteRead, NotConnected 205 | from BaseHTTPServer import BaseHTTPRequestHandler 206 | except ImportError: 207 | # Python 3 208 | from http.cookies import SimpleCookie, CookieError 209 | from http.client import BadStatusLine, HTTPConnection, HTTPSConnection, IncompleteRead, NotConnected 210 | from http.server import BaseHTTPRequestHandler 211 | 212 | try: 213 | # Python 2. We have to do it in this order so Python 2 builds 214 | # don't try to import the 'http' module from cherrypy.lib 215 | from httplib import HTTPSConnection 216 | except ImportError: 217 | try: 218 | # Python 3 219 | from http.client import HTTPSConnection 220 | except ImportError: 221 | # Some platforms which don't have SSL don't expose HTTPSConnection 222 | HTTPSConnection = None 223 | 224 | try: 225 | # Python 2 226 | xrange = xrange 227 | except NameError: 228 | # Python 3 229 | xrange = range 230 | 231 | import threading 232 | if hasattr(threading.Thread, "daemon"): 233 | # Python 2.6+ 234 | def get_daemon(t): 235 | return t.daemon 236 | def set_daemon(t, val): 237 | t.daemon = val 238 | else: 239 | def get_daemon(t): 240 | return t.isDaemon() 241 | def set_daemon(t, val): 242 | t.setDaemon(val) 243 | 244 | try: 245 | from email.utils import formatdate 246 | def HTTPDate(timeval=None): 247 | return formatdate(timeval, usegmt=True) 248 | except ImportError: 249 | from rfc822 import formatdate as HTTPDate 250 | 251 | try: 252 | # Python 3 253 | from urllib.parse import unquote as parse_unquote 254 | def unquote_qs(atom, encoding, errors='strict'): 255 | return parse_unquote(atom.replace('+', ' '), encoding=encoding, errors=errors) 256 | except ImportError: 257 | # Python 2 258 | from urllib import unquote as parse_unquote 259 | def unquote_qs(atom, encoding, errors='strict'): 260 | return parse_unquote(atom.replace('+', ' ')).decode(encoding, errors) 261 | 262 | try: 263 | # Prefer simplejson, which is usually more advanced than the builtin module. 264 | import simplejson as json 265 | json_decode = json.JSONDecoder().decode 266 | json_encode = json.JSONEncoder().iterencode 267 | except ImportError: 268 | if py3k: 269 | # Python 3.0: json is part of the standard library, 270 | # but outputs unicode. We need bytes. 271 | import json 272 | json_decode = json.JSONDecoder().decode 273 | _json_encode = json.JSONEncoder().iterencode 274 | def json_encode(value): 275 | for chunk in _json_encode(value): 276 | yield chunk.encode('utf8') 277 | elif sys.version_info >= (2, 6): 278 | # Python 2.6: json is part of the standard library 279 | import json 280 | json_decode = json.JSONDecoder().decode 281 | json_encode = json.JSONEncoder().iterencode 282 | else: 283 | json = None 284 | def json_decode(s): 285 | raise ValueError('No JSON library is available') 286 | def json_encode(s): 287 | raise ValueError('No JSON library is available') 288 | 289 | try: 290 | import cPickle as pickle 291 | except ImportError: 292 | # In Python 2, pickle is a Python version. 293 | # In Python 3, pickle is the sped-up C version. 294 | import pickle 295 | 296 | try: 297 | os.urandom(20) 298 | import binascii 299 | def random20(): 300 | return binascii.hexlify(os.urandom(20)).decode('ascii') 301 | except (AttributeError, NotImplementedError): 302 | import random 303 | # os.urandom not available until Python 2.4. Fall back to random.random. 304 | def random20(): 305 | return sha('%s' % random.random()).hexdigest() 306 | 307 | try: 308 | from _thread import get_ident as get_thread_ident 309 | except ImportError: 310 | from thread import get_ident as get_thread_ident 311 | 312 | try: 313 | # Python 3 314 | next = next 315 | except NameError: 316 | # Python 2 317 | def next(i): 318 | return i.next() 319 | -------------------------------------------------------------------------------- /cherrypy/_cptree.py: -------------------------------------------------------------------------------- 1 | """CherryPy Application and Tree objects.""" 2 | 3 | import os 4 | import sys 5 | 6 | import cherrypy 7 | from cherrypy._cpcompat import ntou, py3k 8 | from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools 9 | from cherrypy.lib import httputil 10 | 11 | 12 | class Application(object): 13 | """A CherryPy Application. 14 | 15 | Servers and gateways should not instantiate Request objects directly. 16 | Instead, they should ask an Application object for a request object. 17 | 18 | An instance of this class may also be used as a WSGI callable 19 | (WSGI application object) for itself. 20 | """ 21 | 22 | root = None 23 | """The top-most container of page handlers for this app. Handlers should 24 | be arranged in a hierarchy of attributes, matching the expected URI 25 | hierarchy; the default dispatcher then searches this hierarchy for a 26 | matching handler. When using a dispatcher other than the default, 27 | this value may be None.""" 28 | 29 | config = {} 30 | """A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict 31 | of {key: value} pairs.""" 32 | 33 | namespaces = _cpconfig.NamespaceSet() 34 | toolboxes = {'tools': cherrypy.tools} 35 | 36 | log = None 37 | """A LogManager instance. See _cplogging.""" 38 | 39 | wsgiapp = None 40 | """A CPWSGIApp instance. See _cpwsgi.""" 41 | 42 | request_class = _cprequest.Request 43 | response_class = _cprequest.Response 44 | 45 | relative_urls = False 46 | 47 | def __init__(self, root, script_name="", config=None): 48 | self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root) 49 | self.root = root 50 | self.script_name = script_name 51 | self.wsgiapp = _cpwsgi.CPWSGIApp(self) 52 | 53 | self.namespaces = self.namespaces.copy() 54 | self.namespaces["log"] = lambda k, v: setattr(self.log, k, v) 55 | self.namespaces["wsgi"] = self.wsgiapp.namespace_handler 56 | 57 | self.config = self.__class__.config.copy() 58 | if config: 59 | self.merge(config) 60 | 61 | def __repr__(self): 62 | return "%s.%s(%r, %r)" % (self.__module__, self.__class__.__name__, 63 | self.root, self.script_name) 64 | 65 | script_name_doc = """The URI "mount point" for this app. A mount point is that portion of 66 | the URI which is constant for all URIs that are serviced by this 67 | application; it does not include scheme, host, or proxy ("virtual host") 68 | portions of the URI. 69 | 70 | For example, if script_name is "/my/cool/app", then the URL 71 | "http://www.example.com/my/cool/app/page1" might be handled by a 72 | "page1" method on the root object. 73 | 74 | The value of script_name MUST NOT end in a slash. If the script_name 75 | refers to the root of the URI, it MUST be an empty string (not "/"). 76 | 77 | If script_name is explicitly set to None, then the script_name will be 78 | provided for each call from request.wsgi_environ['SCRIPT_NAME']. 79 | """ 80 | def _get_script_name(self): 81 | if self._script_name is None: 82 | # None signals that the script name should be pulled from WSGI environ. 83 | return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip("/") 84 | return self._script_name 85 | def _set_script_name(self, value): 86 | if value: 87 | value = value.rstrip("/") 88 | self._script_name = value 89 | script_name = property(fget=_get_script_name, fset=_set_script_name, 90 | doc=script_name_doc) 91 | 92 | def merge(self, config): 93 | """Merge the given config into self.config.""" 94 | _cpconfig.merge(self.config, config) 95 | 96 | # Handle namespaces specified in config. 97 | self.namespaces(self.config.get("/", {})) 98 | 99 | def find_config(self, path, key, default=None): 100 | """Return the most-specific value for key along path, or default.""" 101 | trail = path or "/" 102 | while trail: 103 | nodeconf = self.config.get(trail, {}) 104 | 105 | if key in nodeconf: 106 | return nodeconf[key] 107 | 108 | lastslash = trail.rfind("/") 109 | if lastslash == -1: 110 | break 111 | elif lastslash == 0 and trail != "/": 112 | trail = "/" 113 | else: 114 | trail = trail[:lastslash] 115 | 116 | return default 117 | 118 | def get_serving(self, local, remote, scheme, sproto): 119 | """Create and return a Request and Response object.""" 120 | req = self.request_class(local, remote, scheme, sproto) 121 | req.app = self 122 | 123 | for name, toolbox in self.toolboxes.items(): 124 | req.namespaces[name] = toolbox 125 | 126 | resp = self.response_class() 127 | cherrypy.serving.load(req, resp) 128 | cherrypy.engine.publish('acquire_thread') 129 | cherrypy.engine.publish('before_request') 130 | 131 | return req, resp 132 | 133 | def release_serving(self): 134 | """Release the current serving (request and response).""" 135 | req = cherrypy.serving.request 136 | 137 | cherrypy.engine.publish('after_request') 138 | 139 | try: 140 | req.close() 141 | except: 142 | cherrypy.log(traceback=True, severity=40) 143 | 144 | cherrypy.serving.clear() 145 | 146 | def __call__(self, environ, start_response): 147 | return self.wsgiapp(environ, start_response) 148 | 149 | 150 | class Tree(object): 151 | """A registry of CherryPy applications, mounted at diverse points. 152 | 153 | An instance of this class may also be used as a WSGI callable 154 | (WSGI application object), in which case it dispatches to all 155 | mounted apps. 156 | """ 157 | 158 | apps = {} 159 | """ 160 | A dict of the form {script name: application}, where "script name" 161 | is a string declaring the URI mount point (no trailing slash), and 162 | "application" is an instance of cherrypy.Application (or an arbitrary 163 | WSGI callable if you happen to be using a WSGI server).""" 164 | 165 | def __init__(self): 166 | self.apps = {} 167 | 168 | def mount(self, root, script_name="", config=None): 169 | """Mount a new app from a root object, script_name, and config. 170 | 171 | root 172 | An instance of a "controller class" (a collection of page 173 | handler methods) which represents the root of the application. 174 | This may also be an Application instance, or None if using 175 | a dispatcher other than the default. 176 | 177 | script_name 178 | A string containing the "mount point" of the application. 179 | This should start with a slash, and be the path portion of the 180 | URL at which to mount the given root. For example, if root.index() 181 | will handle requests to "http://www.example.com:8080/dept/app1/", 182 | then the script_name argument would be "/dept/app1". 183 | 184 | It MUST NOT end in a slash. If the script_name refers to the 185 | root of the URI, it MUST be an empty string (not "/"). 186 | 187 | config 188 | A file or dict containing application config. 189 | """ 190 | if script_name is None: 191 | raise TypeError( 192 | "The 'script_name' argument may not be None. Application " 193 | "objects may, however, possess a script_name of None (in " 194 | "order to inpect the WSGI environ for SCRIPT_NAME upon each " 195 | "request). You cannot mount such Applications on this Tree; " 196 | "you must pass them to a WSGI server interface directly.") 197 | 198 | # Next line both 1) strips trailing slash and 2) maps "/" -> "". 199 | script_name = script_name.rstrip("/") 200 | 201 | if isinstance(root, Application): 202 | app = root 203 | if script_name != "" and script_name != app.script_name: 204 | raise ValueError("Cannot specify a different script name and " 205 | "pass an Application instance to cherrypy.mount") 206 | script_name = app.script_name 207 | else: 208 | app = Application(root, script_name) 209 | 210 | # If mounted at "", add favicon.ico 211 | if (script_name == "" and root is not None 212 | and not hasattr(root, "favicon_ico")): 213 | favicon = os.path.join(os.getcwd(), os.path.dirname(__file__), 214 | "favicon.ico") 215 | root.favicon_ico = tools.staticfile.handler(favicon) 216 | 217 | if config: 218 | app.merge(config) 219 | 220 | self.apps[script_name] = app 221 | 222 | return app 223 | 224 | def graft(self, wsgi_callable, script_name=""): 225 | """Mount a wsgi callable at the given script_name.""" 226 | # Next line both 1) strips trailing slash and 2) maps "/" -> "". 227 | script_name = script_name.rstrip("/") 228 | self.apps[script_name] = wsgi_callable 229 | 230 | def script_name(self, path=None): 231 | """The script_name of the app at the given path, or None. 232 | 233 | If path is None, cherrypy.request is used. 234 | """ 235 | if path is None: 236 | try: 237 | request = cherrypy.serving.request 238 | path = httputil.urljoin(request.script_name, 239 | request.path_info) 240 | except AttributeError: 241 | return None 242 | 243 | while True: 244 | if path in self.apps: 245 | return path 246 | 247 | if path == "": 248 | return None 249 | 250 | # Move one node up the tree and try again. 251 | path = path[:path.rfind("/")] 252 | 253 | def __call__(self, environ, start_response): 254 | # If you're calling this, then you're probably setting SCRIPT_NAME 255 | # to '' (some WSGI servers always set SCRIPT_NAME to ''). 256 | # Try to look up the app using the full path. 257 | env1x = environ 258 | if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): 259 | env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ) 260 | path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''), 261 | env1x.get('PATH_INFO', '')) 262 | sn = self.script_name(path or "/") 263 | if sn is None: 264 | start_response('404 Not Found', []) 265 | return [] 266 | 267 | app = self.apps[sn] 268 | 269 | # Correct the SCRIPT_NAME and PATH_INFO environ entries. 270 | environ = environ.copy() 271 | if not py3k: 272 | if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): 273 | # Python 2/WSGI u.0: all strings MUST be of type unicode 274 | enc = environ[ntou('wsgi.url_encoding')] 275 | environ[ntou('SCRIPT_NAME')] = sn.decode(enc) 276 | environ[ntou('PATH_INFO')] = path[len(sn.rstrip("/")):].decode(enc) 277 | else: 278 | # Python 2/WSGI 1.x: all strings MUST be of type str 279 | environ['SCRIPT_NAME'] = sn 280 | environ['PATH_INFO'] = path[len(sn.rstrip("/")):] 281 | else: 282 | if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): 283 | # Python 3/WSGI u.0: all strings MUST be full unicode 284 | environ['SCRIPT_NAME'] = sn 285 | environ['PATH_INFO'] = path[len(sn.rstrip("/")):] 286 | else: 287 | # Python 3/WSGI 1.x: all strings MUST be ISO-8859-1 str 288 | environ['SCRIPT_NAME'] = sn.encode('utf-8').decode('ISO-8859-1') 289 | environ['PATH_INFO'] = path[len(sn.rstrip("/")):].encode('utf-8').decode('ISO-8859-1') 290 | return app(environ, start_response) 291 | -------------------------------------------------------------------------------- /cherrypy/_cpmodpy.py: -------------------------------------------------------------------------------- 1 | """Native adapter for serving CherryPy via mod_python 2 | 3 | Basic usage: 4 | 5 | ########################################## 6 | # Application in a module called myapp.py 7 | ########################################## 8 | 9 | import cherrypy 10 | 11 | class Root: 12 | @cherrypy.expose 13 | def index(self): 14 | return 'Hi there, Ho there, Hey there' 15 | 16 | 17 | # We will use this method from the mod_python configuration 18 | # as the entry point to our application 19 | def setup_server(): 20 | cherrypy.tree.mount(Root()) 21 | cherrypy.config.update({'environment': 'production', 22 | 'log.screen': False, 23 | 'show_tracebacks': False}) 24 | 25 | ########################################## 26 | # mod_python settings for apache2 27 | # This should reside in your httpd.conf 28 | # or a file that will be loaded at 29 | # apache startup 30 | ########################################## 31 | 32 | # Start 33 | DocumentRoot "/" 34 | Listen 8080 35 | LoadModule python_module /usr/lib/apache2/modules/mod_python.so 36 | 37 | 38 | PythonPath "sys.path+['/path/to/my/application']" 39 | SetHandler python-program 40 | PythonHandler cherrypy._cpmodpy::handler 41 | PythonOption cherrypy.setup myapp::setup_server 42 | PythonDebug On 43 | 44 | # End 45 | 46 | The actual path to your mod_python.so is dependent on your 47 | environment. In this case we suppose a global mod_python 48 | installation on a Linux distribution such as Ubuntu. 49 | 50 | We do set the PythonPath configuration setting so that 51 | your application can be found by from the user running 52 | the apache2 instance. Of course if your application 53 | resides in the global site-package this won't be needed. 54 | 55 | Then restart apache2 and access http://127.0.0.1:8080 56 | """ 57 | 58 | import logging 59 | import sys 60 | 61 | import cherrypy 62 | from cherrypy._cpcompat import BytesIO, copyitems, ntob 63 | from cherrypy._cperror import format_exc, bare_error 64 | from cherrypy.lib import httputil 65 | 66 | 67 | # ------------------------------ Request-handling 68 | 69 | 70 | 71 | def setup(req): 72 | from mod_python import apache 73 | 74 | # Run any setup functions defined by a "PythonOption cherrypy.setup" directive. 75 | options = req.get_options() 76 | if 'cherrypy.setup' in options: 77 | for function in options['cherrypy.setup'].split(): 78 | atoms = function.split('::', 1) 79 | if len(atoms) == 1: 80 | mod = __import__(atoms[0], globals(), locals()) 81 | else: 82 | modname, fname = atoms 83 | mod = __import__(modname, globals(), locals(), [fname]) 84 | func = getattr(mod, fname) 85 | func() 86 | 87 | cherrypy.config.update({'log.screen': False, 88 | "tools.ignore_headers.on": True, 89 | "tools.ignore_headers.headers": ['Range'], 90 | }) 91 | 92 | engine = cherrypy.engine 93 | if hasattr(engine, "signal_handler"): 94 | engine.signal_handler.unsubscribe() 95 | if hasattr(engine, "console_control_handler"): 96 | engine.console_control_handler.unsubscribe() 97 | engine.autoreload.unsubscribe() 98 | cherrypy.server.unsubscribe() 99 | 100 | def _log(msg, level): 101 | newlevel = apache.APLOG_ERR 102 | if logging.DEBUG >= level: 103 | newlevel = apache.APLOG_DEBUG 104 | elif logging.INFO >= level: 105 | newlevel = apache.APLOG_INFO 106 | elif logging.WARNING >= level: 107 | newlevel = apache.APLOG_WARNING 108 | # On Windows, req.server is required or the msg will vanish. See 109 | # http://www.modpython.org/pipermail/mod_python/2003-October/014291.html. 110 | # Also, "When server is not specified...LogLevel does not apply..." 111 | apache.log_error(msg, newlevel, req.server) 112 | engine.subscribe('log', _log) 113 | 114 | engine.start() 115 | 116 | def cherrypy_cleanup(data): 117 | engine.exit() 118 | try: 119 | # apache.register_cleanup wasn't available until 3.1.4. 120 | apache.register_cleanup(cherrypy_cleanup) 121 | except AttributeError: 122 | req.server.register_cleanup(req, cherrypy_cleanup) 123 | 124 | 125 | class _ReadOnlyRequest: 126 | expose = ('read', 'readline', 'readlines') 127 | def __init__(self, req): 128 | for method in self.expose: 129 | self.__dict__[method] = getattr(req, method) 130 | 131 | 132 | recursive = False 133 | 134 | _isSetUp = False 135 | def handler(req): 136 | from mod_python import apache 137 | try: 138 | global _isSetUp 139 | if not _isSetUp: 140 | setup(req) 141 | _isSetUp = True 142 | 143 | # Obtain a Request object from CherryPy 144 | local = req.connection.local_addr 145 | local = httputil.Host(local[0], local[1], req.connection.local_host or "") 146 | remote = req.connection.remote_addr 147 | remote = httputil.Host(remote[0], remote[1], req.connection.remote_host or "") 148 | 149 | scheme = req.parsed_uri[0] or 'http' 150 | req.get_basic_auth_pw() 151 | 152 | try: 153 | # apache.mpm_query only became available in mod_python 3.1 154 | q = apache.mpm_query 155 | threaded = q(apache.AP_MPMQ_IS_THREADED) 156 | forked = q(apache.AP_MPMQ_IS_FORKED) 157 | except AttributeError: 158 | bad_value = ("You must provide a PythonOption '%s', " 159 | "either 'on' or 'off', when running a version " 160 | "of mod_python < 3.1") 161 | 162 | threaded = options.get('multithread', '').lower() 163 | if threaded == 'on': 164 | threaded = True 165 | elif threaded == 'off': 166 | threaded = False 167 | else: 168 | raise ValueError(bad_value % "multithread") 169 | 170 | forked = options.get('multiprocess', '').lower() 171 | if forked == 'on': 172 | forked = True 173 | elif forked == 'off': 174 | forked = False 175 | else: 176 | raise ValueError(bad_value % "multiprocess") 177 | 178 | sn = cherrypy.tree.script_name(req.uri or "/") 179 | if sn is None: 180 | send_response(req, '404 Not Found', [], '') 181 | else: 182 | app = cherrypy.tree.apps[sn] 183 | method = req.method 184 | path = req.uri 185 | qs = req.args or "" 186 | reqproto = req.protocol 187 | headers = copyitems(req.headers_in) 188 | rfile = _ReadOnlyRequest(req) 189 | prev = None 190 | 191 | try: 192 | redirections = [] 193 | while True: 194 | request, response = app.get_serving(local, remote, scheme, 195 | "HTTP/1.1") 196 | request.login = req.user 197 | request.multithread = bool(threaded) 198 | request.multiprocess = bool(forked) 199 | request.app = app 200 | request.prev = prev 201 | 202 | # Run the CherryPy Request object and obtain the response 203 | try: 204 | request.run(method, path, qs, reqproto, headers, rfile) 205 | break 206 | except cherrypy.InternalRedirect: 207 | ir = sys.exc_info()[1] 208 | app.release_serving() 209 | prev = request 210 | 211 | if not recursive: 212 | if ir.path in redirections: 213 | raise RuntimeError("InternalRedirector visited the " 214 | "same URL twice: %r" % ir.path) 215 | else: 216 | # Add the *previous* path_info + qs to redirections. 217 | if qs: 218 | qs = "?" + qs 219 | redirections.append(sn + path + qs) 220 | 221 | # Munge environment and try again. 222 | method = "GET" 223 | path = ir.path 224 | qs = ir.query_string 225 | rfile = BytesIO() 226 | 227 | send_response(req, response.output_status, response.header_list, 228 | response.body, response.stream) 229 | finally: 230 | app.release_serving() 231 | except: 232 | tb = format_exc() 233 | cherrypy.log(tb, 'MOD_PYTHON', severity=logging.ERROR) 234 | s, h, b = bare_error() 235 | send_response(req, s, h, b) 236 | return apache.OK 237 | 238 | 239 | def send_response(req, status, headers, body, stream=False): 240 | # Set response status 241 | req.status = int(status[:3]) 242 | 243 | # Set response headers 244 | req.content_type = "text/plain" 245 | for header, value in headers: 246 | if header.lower() == 'content-type': 247 | req.content_type = value 248 | continue 249 | req.headers_out.add(header, value) 250 | 251 | if stream: 252 | # Flush now so the status and headers are sent immediately. 253 | req.flush() 254 | 255 | # Set response body 256 | if isinstance(body, basestring): 257 | req.write(body) 258 | else: 259 | for seg in body: 260 | req.write(seg) 261 | 262 | 263 | 264 | # --------------- Startup tools for CherryPy + mod_python --------------- # 265 | 266 | 267 | import os 268 | import re 269 | try: 270 | import subprocess 271 | def popen(fullcmd): 272 | p = subprocess.Popen(fullcmd, shell=True, 273 | stdout=subprocess.PIPE, stderr=subprocess.STDOUT, 274 | close_fds=True) 275 | return p.stdout 276 | except ImportError: 277 | def popen(fullcmd): 278 | pipein, pipeout = os.popen4(fullcmd) 279 | return pipeout 280 | 281 | 282 | def read_process(cmd, args=""): 283 | fullcmd = "%s %s" % (cmd, args) 284 | pipeout = popen(fullcmd) 285 | try: 286 | firstline = pipeout.readline() 287 | if (re.search(ntob("(not recognized|No such file|not found)"), firstline, 288 | re.IGNORECASE)): 289 | raise IOError('%s must be on your system path.' % cmd) 290 | output = firstline + pipeout.read() 291 | finally: 292 | pipeout.close() 293 | return output 294 | 295 | 296 | class ModPythonServer(object): 297 | 298 | template = """ 299 | # Apache2 server configuration file for running CherryPy with mod_python. 300 | 301 | DocumentRoot "/" 302 | Listen %(port)s 303 | LoadModule python_module modules/mod_python.so 304 | 305 | 306 | SetHandler python-program 307 | PythonHandler %(handler)s 308 | PythonDebug On 309 | %(opts)s 310 | 311 | """ 312 | 313 | def __init__(self, loc="/", port=80, opts=None, apache_path="apache", 314 | handler="cherrypy._cpmodpy::handler"): 315 | self.loc = loc 316 | self.port = port 317 | self.opts = opts 318 | self.apache_path = apache_path 319 | self.handler = handler 320 | 321 | def start(self): 322 | opts = "".join([" PythonOption %s %s\n" % (k, v) 323 | for k, v in self.opts]) 324 | conf_data = self.template % {"port": self.port, 325 | "loc": self.loc, 326 | "opts": opts, 327 | "handler": self.handler, 328 | } 329 | 330 | mpconf = os.path.join(os.path.dirname(__file__), "cpmodpy.conf") 331 | f = open(mpconf, 'wb') 332 | try: 333 | f.write(conf_data) 334 | finally: 335 | f.close() 336 | 337 | response = read_process(self.apache_path, "-k start -f %s" % mpconf) 338 | self.ready = True 339 | return response 340 | 341 | def stop(self): 342 | os.popen("apache -k stop") 343 | self.ready = False 344 | 345 | -------------------------------------------------------------------------------- /cherrypy/lib/covercp.py: -------------------------------------------------------------------------------- 1 | """Code-coverage tools for CherryPy. 2 | 3 | To use this module, or the coverage tools in the test suite, 4 | you need to download 'coverage.py', either Gareth Rees' `original 5 | implementation `_ 6 | or Ned Batchelder's `enhanced version: 7 | `_ 8 | 9 | To turn on coverage tracing, use the following code:: 10 | 11 | cherrypy.engine.subscribe('start', covercp.start) 12 | 13 | DO NOT subscribe anything on the 'start_thread' channel, as previously 14 | recommended. Calling start once in the main thread should be sufficient 15 | to start coverage on all threads. Calling start again in each thread 16 | effectively clears any coverage data gathered up to that point. 17 | 18 | Run your code, then use the ``covercp.serve()`` function to browse the 19 | results in a web browser. If you run this module from the command line, 20 | it will call ``serve()`` for you. 21 | """ 22 | 23 | import re 24 | import sys 25 | import cgi 26 | from cherrypy._cpcompat import quote_plus 27 | import os, os.path 28 | localFile = os.path.join(os.path.dirname(__file__), "coverage.cache") 29 | 30 | the_coverage = None 31 | try: 32 | from coverage import coverage 33 | the_coverage = coverage(data_file=localFile) 34 | def start(): 35 | the_coverage.start() 36 | except ImportError: 37 | # Setting the_coverage to None will raise errors 38 | # that need to be trapped downstream. 39 | the_coverage = None 40 | 41 | import warnings 42 | warnings.warn("No code coverage will be performed; coverage.py could not be imported.") 43 | 44 | def start(): 45 | pass 46 | start.priority = 20 47 | 48 | TEMPLATE_MENU = """ 49 | 50 | CherryPy Coverage Menu 51 | 113 | 114 | 115 |

CherryPy Coverage

""" 116 | 117 | TEMPLATE_FORM = """ 118 |
119 |
120 | 121 | Show percentages
122 | Hide files over %%
123 | Exclude files matching
124 | 125 |
126 | 127 | 128 |
129 |
""" 130 | 131 | TEMPLATE_FRAMESET = """ 132 | CherryPy coverage data 133 | 134 | 135 | 136 | 137 | 138 | """ 139 | 140 | TEMPLATE_COVERAGE = """ 141 | 142 | Coverage for %(name)s 143 | 157 | 158 | 159 |

%(name)s

160 |

%(fullpath)s

161 |

Coverage: %(pc)s%%

""" 162 | 163 | TEMPLATE_LOC_COVERED = """ 164 | %s  165 | %s 166 | \n""" 167 | TEMPLATE_LOC_NOT_COVERED = """ 168 | %s  169 | %s 170 | \n""" 171 | TEMPLATE_LOC_EXCLUDED = """ 172 | %s  173 | %s 174 | \n""" 175 | 176 | TEMPLATE_ITEM = "%s%s%s\n" 177 | 178 | def _percent(statements, missing): 179 | s = len(statements) 180 | e = s - len(missing) 181 | if s > 0: 182 | return int(round(100.0 * e / s)) 183 | return 0 184 | 185 | def _show_branch(root, base, path, pct=0, showpct=False, exclude="", 186 | coverage=the_coverage): 187 | 188 | # Show the directory name and any of our children 189 | dirs = [k for k, v in root.items() if v] 190 | dirs.sort() 191 | for name in dirs: 192 | newpath = os.path.join(path, name) 193 | 194 | if newpath.lower().startswith(base): 195 | relpath = newpath[len(base):] 196 | yield "| " * relpath.count(os.sep) 197 | yield "%s\n" % \ 198 | (newpath, quote_plus(exclude), name) 199 | 200 | for chunk in _show_branch(root[name], base, newpath, pct, showpct, exclude, coverage=coverage): 201 | yield chunk 202 | 203 | # Now list the files 204 | if path.lower().startswith(base): 205 | relpath = path[len(base):] 206 | files = [k for k, v in root.items() if not v] 207 | files.sort() 208 | for name in files: 209 | newpath = os.path.join(path, name) 210 | 211 | pc_str = "" 212 | if showpct: 213 | try: 214 | _, statements, _, missing, _ = coverage.analysis2(newpath) 215 | except: 216 | # Yes, we really want to pass on all errors. 217 | pass 218 | else: 219 | pc = _percent(statements, missing) 220 | pc_str = ("%3d%% " % pc).replace(' ',' ') 221 | if pc < float(pct) or pc == -1: 222 | pc_str = "%s" % pc_str 223 | else: 224 | pc_str = "%s" % pc_str 225 | 226 | yield TEMPLATE_ITEM % ("| " * (relpath.count(os.sep) + 1), 227 | pc_str, newpath, name) 228 | 229 | def _skip_file(path, exclude): 230 | if exclude: 231 | return bool(re.search(exclude, path)) 232 | 233 | def _graft(path, tree): 234 | d = tree 235 | 236 | p = path 237 | atoms = [] 238 | while True: 239 | p, tail = os.path.split(p) 240 | if not tail: 241 | break 242 | atoms.append(tail) 243 | atoms.append(p) 244 | if p != "/": 245 | atoms.append("/") 246 | 247 | atoms.reverse() 248 | for node in atoms: 249 | if node: 250 | d = d.setdefault(node, {}) 251 | 252 | def get_tree(base, exclude, coverage=the_coverage): 253 | """Return covered module names as a nested dict.""" 254 | tree = {} 255 | runs = coverage.data.executed_files() 256 | for path in runs: 257 | if not _skip_file(path, exclude) and not os.path.isdir(path): 258 | _graft(path, tree) 259 | return tree 260 | 261 | class CoverStats(object): 262 | 263 | def __init__(self, coverage, root=None): 264 | self.coverage = coverage 265 | if root is None: 266 | # Guess initial depth. Files outside this path will not be 267 | # reachable from the web interface. 268 | import cherrypy 269 | root = os.path.dirname(cherrypy.__file__) 270 | self.root = root 271 | 272 | def index(self): 273 | return TEMPLATE_FRAMESET % self.root.lower() 274 | index.exposed = True 275 | 276 | def menu(self, base="/", pct="50", showpct="", 277 | exclude=r'python\d\.\d|test|tut\d|tutorial'): 278 | 279 | # The coverage module uses all-lower-case names. 280 | base = base.lower().rstrip(os.sep) 281 | 282 | yield TEMPLATE_MENU 283 | yield TEMPLATE_FORM % locals() 284 | 285 | # Start by showing links for parent paths 286 | yield "
" 287 | path = "" 288 | atoms = base.split(os.sep) 289 | atoms.pop() 290 | for atom in atoms: 291 | path += atom + os.sep 292 | yield ("%s %s" 293 | % (path, quote_plus(exclude), atom, os.sep)) 294 | yield "
" 295 | 296 | yield "
" 297 | 298 | # Then display the tree 299 | tree = get_tree(base, exclude, self.coverage) 300 | if not tree: 301 | yield "

No modules covered.

" 302 | else: 303 | for chunk in _show_branch(tree, base, "/", pct, 304 | showpct=='checked', exclude, coverage=self.coverage): 305 | yield chunk 306 | 307 | yield "
" 308 | yield "" 309 | menu.exposed = True 310 | 311 | def annotated_file(self, filename, statements, excluded, missing): 312 | source = open(filename, 'r') 313 | buffer = [] 314 | for lineno, line in enumerate(source.readlines()): 315 | lineno += 1 316 | line = line.strip("\n\r") 317 | empty_the_buffer = True 318 | if lineno in excluded: 319 | template = TEMPLATE_LOC_EXCLUDED 320 | elif lineno in missing: 321 | template = TEMPLATE_LOC_NOT_COVERED 322 | elif lineno in statements: 323 | template = TEMPLATE_LOC_COVERED 324 | else: 325 | empty_the_buffer = False 326 | buffer.append((lineno, line)) 327 | if empty_the_buffer: 328 | for lno, pastline in buffer: 329 | yield template % (lno, cgi.escape(pastline)) 330 | buffer = [] 331 | yield template % (lineno, cgi.escape(line)) 332 | 333 | def report(self, name): 334 | filename, statements, excluded, missing, _ = self.coverage.analysis2(name) 335 | pc = _percent(statements, missing) 336 | yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name), 337 | fullpath=name, 338 | pc=pc) 339 | yield '\n' 340 | for line in self.annotated_file(filename, statements, excluded, 341 | missing): 342 | yield line 343 | yield '
' 344 | yield '' 345 | yield '' 346 | report.exposed = True 347 | 348 | 349 | def serve(path=localFile, port=8080, root=None): 350 | if coverage is None: 351 | raise ImportError("The coverage module could not be imported.") 352 | from coverage import coverage 353 | cov = coverage(data_file = path) 354 | cov.load() 355 | 356 | import cherrypy 357 | cherrypy.config.update({'server.socket_port': int(port), 358 | 'server.thread_pool': 10, 359 | 'environment': "production", 360 | }) 361 | cherrypy.quickstart(CoverStats(cov, root)) 362 | 363 | if __name__ == "__main__": 364 | serve(*tuple(sys.argv[1:])) 365 | 366 | -------------------------------------------------------------------------------- /cherrypy/lib/httpauth.py: -------------------------------------------------------------------------------- 1 | """ 2 | This module defines functions to implement HTTP Digest Authentication (:rfc:`2617`). 3 | This has full compliance with 'Digest' and 'Basic' authentication methods. In 4 | 'Digest' it supports both MD5 and MD5-sess algorithms. 5 | 6 | Usage: 7 | First use 'doAuth' to request the client authentication for a 8 | certain resource. You should send an httplib.UNAUTHORIZED response to the 9 | client so he knows he has to authenticate itself. 10 | 11 | Then use 'parseAuthorization' to retrieve the 'auth_map' used in 12 | 'checkResponse'. 13 | 14 | To use 'checkResponse' you must have already verified the password associated 15 | with the 'username' key in 'auth_map' dict. Then you use the 'checkResponse' 16 | function to verify if the password matches the one sent by the client. 17 | 18 | SUPPORTED_ALGORITHM - list of supported 'Digest' algorithms 19 | SUPPORTED_QOP - list of supported 'Digest' 'qop'. 20 | """ 21 | __version__ = 1, 0, 1 22 | __author__ = "Tiago Cogumbreiro " 23 | __credits__ = """ 24 | Peter van Kampen for its recipe which implement most of Digest authentication: 25 | http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/302378 26 | """ 27 | 28 | __license__ = """ 29 | Copyright (c) 2005, Tiago Cogumbreiro 30 | All rights reserved. 31 | 32 | Redistribution and use in source and binary forms, with or without modification, 33 | are permitted provided that the following conditions are met: 34 | 35 | * Redistributions of source code must retain the above copyright notice, 36 | this list of conditions and the following disclaimer. 37 | * Redistributions in binary form must reproduce the above copyright notice, 38 | this list of conditions and the following disclaimer in the documentation 39 | and/or other materials provided with the distribution. 40 | * Neither the name of Sylvain Hellegouarch nor the names of his contributors 41 | may be used to endorse or promote products derived from this software 42 | without specific prior written permission. 43 | 44 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 45 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 46 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 47 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 48 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 49 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 50 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 51 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 52 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 53 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 54 | """ 55 | 56 | __all__ = ("digestAuth", "basicAuth", "doAuth", "checkResponse", 57 | "parseAuthorization", "SUPPORTED_ALGORITHM", "md5SessionKey", 58 | "calculateNonce", "SUPPORTED_QOP") 59 | 60 | ################################################################################ 61 | import time 62 | from cherrypy._cpcompat import base64_decode, ntob, md5 63 | from cherrypy._cpcompat import parse_http_list, parse_keqv_list 64 | 65 | MD5 = "MD5" 66 | MD5_SESS = "MD5-sess" 67 | AUTH = "auth" 68 | AUTH_INT = "auth-int" 69 | 70 | SUPPORTED_ALGORITHM = (MD5, MD5_SESS) 71 | SUPPORTED_QOP = (AUTH, AUTH_INT) 72 | 73 | ################################################################################ 74 | # doAuth 75 | # 76 | DIGEST_AUTH_ENCODERS = { 77 | MD5: lambda val: md5(ntob(val)).hexdigest(), 78 | MD5_SESS: lambda val: md5(ntob(val)).hexdigest(), 79 | # SHA: lambda val: sha.new(ntob(val)).hexdigest (), 80 | } 81 | 82 | def calculateNonce (realm, algorithm = MD5): 83 | """This is an auxaliary function that calculates 'nonce' value. It is used 84 | to handle sessions.""" 85 | 86 | global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS 87 | assert algorithm in SUPPORTED_ALGORITHM 88 | 89 | try: 90 | encoder = DIGEST_AUTH_ENCODERS[algorithm] 91 | except KeyError: 92 | raise NotImplementedError ("The chosen algorithm (%s) does not have "\ 93 | "an implementation yet" % algorithm) 94 | 95 | return encoder ("%d:%s" % (time.time(), realm)) 96 | 97 | def digestAuth (realm, algorithm = MD5, nonce = None, qop = AUTH): 98 | """Challenges the client for a Digest authentication.""" 99 | global SUPPORTED_ALGORITHM, DIGEST_AUTH_ENCODERS, SUPPORTED_QOP 100 | assert algorithm in SUPPORTED_ALGORITHM 101 | assert qop in SUPPORTED_QOP 102 | 103 | if nonce is None: 104 | nonce = calculateNonce (realm, algorithm) 105 | 106 | return 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % ( 107 | realm, nonce, algorithm, qop 108 | ) 109 | 110 | def basicAuth (realm): 111 | """Challengenes the client for a Basic authentication.""" 112 | assert '"' not in realm, "Realms cannot contain the \" (quote) character." 113 | 114 | return 'Basic realm="%s"' % realm 115 | 116 | def doAuth (realm): 117 | """'doAuth' function returns the challenge string b giving priority over 118 | Digest and fallback to Basic authentication when the browser doesn't 119 | support the first one. 120 | 121 | This should be set in the HTTP header under the key 'WWW-Authenticate'.""" 122 | 123 | return digestAuth (realm) + " " + basicAuth (realm) 124 | 125 | 126 | ################################################################################ 127 | # Parse authorization parameters 128 | # 129 | def _parseDigestAuthorization (auth_params): 130 | # Convert the auth params to a dict 131 | items = parse_http_list(auth_params) 132 | params = parse_keqv_list(items) 133 | 134 | # Now validate the params 135 | 136 | # Check for required parameters 137 | required = ["username", "realm", "nonce", "uri", "response"] 138 | for k in required: 139 | if k not in params: 140 | return None 141 | 142 | # If qop is sent then cnonce and nc MUST be present 143 | if "qop" in params and not ("cnonce" in params \ 144 | and "nc" in params): 145 | return None 146 | 147 | # If qop is not sent, neither cnonce nor nc can be present 148 | if ("cnonce" in params or "nc" in params) and \ 149 | "qop" not in params: 150 | return None 151 | 152 | return params 153 | 154 | 155 | def _parseBasicAuthorization (auth_params): 156 | username, password = base64_decode(auth_params).split(":", 1) 157 | return {"username": username, "password": password} 158 | 159 | AUTH_SCHEMES = { 160 | "basic": _parseBasicAuthorization, 161 | "digest": _parseDigestAuthorization, 162 | } 163 | 164 | def parseAuthorization (credentials): 165 | """parseAuthorization will convert the value of the 'Authorization' key in 166 | the HTTP header to a map itself. If the parsing fails 'None' is returned. 167 | """ 168 | 169 | global AUTH_SCHEMES 170 | 171 | auth_scheme, auth_params = credentials.split(" ", 1) 172 | auth_scheme = auth_scheme.lower () 173 | 174 | parser = AUTH_SCHEMES[auth_scheme] 175 | params = parser (auth_params) 176 | 177 | if params is None: 178 | return 179 | 180 | assert "auth_scheme" not in params 181 | params["auth_scheme"] = auth_scheme 182 | return params 183 | 184 | 185 | ################################################################################ 186 | # Check provided response for a valid password 187 | # 188 | def md5SessionKey (params, password): 189 | """ 190 | If the "algorithm" directive's value is "MD5-sess", then A1 191 | [the session key] is calculated only once - on the first request by the 192 | client following receipt of a WWW-Authenticate challenge from the server. 193 | 194 | This creates a 'session key' for the authentication of subsequent 195 | requests and responses which is different for each "authentication 196 | session", thus limiting the amount of material hashed with any one 197 | key. 198 | 199 | Because the server need only use the hash of the user 200 | credentials in order to create the A1 value, this construction could 201 | be used in conjunction with a third party authentication service so 202 | that the web server would not need the actual password value. The 203 | specification of such a protocol is beyond the scope of this 204 | specification. 205 | """ 206 | 207 | keys = ("username", "realm", "nonce", "cnonce") 208 | params_copy = {} 209 | for key in keys: 210 | params_copy[key] = params[key] 211 | 212 | params_copy["algorithm"] = MD5_SESS 213 | return _A1 (params_copy, password) 214 | 215 | def _A1(params, password): 216 | algorithm = params.get ("algorithm", MD5) 217 | H = DIGEST_AUTH_ENCODERS[algorithm] 218 | 219 | if algorithm == MD5: 220 | # If the "algorithm" directive's value is "MD5" or is 221 | # unspecified, then A1 is: 222 | # A1 = unq(username-value) ":" unq(realm-value) ":" passwd 223 | return "%s:%s:%s" % (params["username"], params["realm"], password) 224 | 225 | elif algorithm == MD5_SESS: 226 | 227 | # This is A1 if qop is set 228 | # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd ) 229 | # ":" unq(nonce-value) ":" unq(cnonce-value) 230 | h_a1 = H ("%s:%s:%s" % (params["username"], params["realm"], password)) 231 | return "%s:%s:%s" % (h_a1, params["nonce"], params["cnonce"]) 232 | 233 | 234 | def _A2(params, method, kwargs): 235 | # If the "qop" directive's value is "auth" or is unspecified, then A2 is: 236 | # A2 = Method ":" digest-uri-value 237 | 238 | qop = params.get ("qop", "auth") 239 | if qop == "auth": 240 | return method + ":" + params["uri"] 241 | elif qop == "auth-int": 242 | # If the "qop" value is "auth-int", then A2 is: 243 | # A2 = Method ":" digest-uri-value ":" H(entity-body) 244 | entity_body = kwargs.get ("entity_body", "") 245 | H = kwargs["H"] 246 | 247 | return "%s:%s:%s" % ( 248 | method, 249 | params["uri"], 250 | H(entity_body) 251 | ) 252 | 253 | else: 254 | raise NotImplementedError ("The 'qop' method is unknown: %s" % qop) 255 | 256 | def _computeDigestResponse(auth_map, password, method = "GET", A1 = None,**kwargs): 257 | """ 258 | Generates a response respecting the algorithm defined in RFC 2617 259 | """ 260 | params = auth_map 261 | 262 | algorithm = params.get ("algorithm", MD5) 263 | 264 | H = DIGEST_AUTH_ENCODERS[algorithm] 265 | KD = lambda secret, data: H(secret + ":" + data) 266 | 267 | qop = params.get ("qop", None) 268 | 269 | H_A2 = H(_A2(params, method, kwargs)) 270 | 271 | if algorithm == MD5_SESS and A1 is not None: 272 | H_A1 = H(A1) 273 | else: 274 | H_A1 = H(_A1(params, password)) 275 | 276 | if qop in ("auth", "auth-int"): 277 | # If the "qop" value is "auth" or "auth-int": 278 | # request-digest = <"> < KD ( H(A1), unq(nonce-value) 279 | # ":" nc-value 280 | # ":" unq(cnonce-value) 281 | # ":" unq(qop-value) 282 | # ":" H(A2) 283 | # ) <"> 284 | request = "%s:%s:%s:%s:%s" % ( 285 | params["nonce"], 286 | params["nc"], 287 | params["cnonce"], 288 | params["qop"], 289 | H_A2, 290 | ) 291 | elif qop is None: 292 | # If the "qop" directive is not present (this construction is 293 | # for compatibility with RFC 2069): 294 | # request-digest = 295 | # <"> < KD ( H(A1), unq(nonce-value) ":" H(A2) ) > <"> 296 | request = "%s:%s" % (params["nonce"], H_A2) 297 | 298 | return KD(H_A1, request) 299 | 300 | def _checkDigestResponse(auth_map, password, method = "GET", A1 = None, **kwargs): 301 | """This function is used to verify the response given by the client when 302 | he tries to authenticate. 303 | Optional arguments: 304 | entity_body - when 'qop' is set to 'auth-int' you MUST provide the 305 | raw data you are going to send to the client (usually the 306 | HTML page. 307 | request_uri - the uri from the request line compared with the 'uri' 308 | directive of the authorization map. They must represent 309 | the same resource (unused at this time). 310 | """ 311 | 312 | if auth_map['realm'] != kwargs.get('realm', None): 313 | return False 314 | 315 | response = _computeDigestResponse(auth_map, password, method, A1,**kwargs) 316 | 317 | return response == auth_map["response"] 318 | 319 | def _checkBasicResponse (auth_map, password, method='GET', encrypt=None, **kwargs): 320 | # Note that the Basic response doesn't provide the realm value so we cannot 321 | # test it 322 | try: 323 | return encrypt(auth_map["password"], auth_map["username"]) == password 324 | except TypeError: 325 | return encrypt(auth_map["password"]) == password 326 | 327 | AUTH_RESPONSES = { 328 | "basic": _checkBasicResponse, 329 | "digest": _checkDigestResponse, 330 | } 331 | 332 | def checkResponse (auth_map, password, method = "GET", encrypt=None, **kwargs): 333 | """'checkResponse' compares the auth_map with the password and optionally 334 | other arguments that each implementation might need. 335 | 336 | If the response is of type 'Basic' then the function has the following 337 | signature:: 338 | 339 | checkBasicResponse (auth_map, password) -> bool 340 | 341 | If the response is of type 'Digest' then the function has the following 342 | signature:: 343 | 344 | checkDigestResponse (auth_map, password, method = 'GET', A1 = None) -> bool 345 | 346 | The 'A1' argument is only used in MD5_SESS algorithm based responses. 347 | Check md5SessionKey() for more info. 348 | """ 349 | checker = AUTH_RESPONSES[auth_map["auth_scheme"]] 350 | return checker (auth_map, password, method=method, encrypt=encrypt, **kwargs) 351 | 352 | 353 | 354 | 355 | -------------------------------------------------------------------------------- /cherrypy/lib/auth_digest.py: -------------------------------------------------------------------------------- 1 | # This file is part of CherryPy 2 | # -*- coding: utf-8 -*- 3 | # vim:ts=4:sw=4:expandtab:fileencoding=utf-8 4 | 5 | __doc__ = """An implementation of the server-side of HTTP Digest Access 6 | Authentication, which is described in :rfc:`2617`. 7 | 8 | Example usage, using the built-in get_ha1_dict_plain function which uses a dict 9 | of plaintext passwords as the credentials store:: 10 | 11 | userpassdict = {'alice' : '4x5istwelve'} 12 | get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(userpassdict) 13 | digest_auth = {'tools.auth_digest.on': True, 14 | 'tools.auth_digest.realm': 'wonderland', 15 | 'tools.auth_digest.get_ha1': get_ha1, 16 | 'tools.auth_digest.key': 'a565c27146791cfb', 17 | } 18 | app_config = { '/' : digest_auth } 19 | """ 20 | 21 | __author__ = 'visteya' 22 | __date__ = 'April 2009' 23 | 24 | 25 | import time 26 | from cherrypy._cpcompat import parse_http_list, parse_keqv_list 27 | 28 | import cherrypy 29 | from cherrypy._cpcompat import md5, ntob 30 | md5_hex = lambda s: md5(ntob(s)).hexdigest() 31 | 32 | qop_auth = 'auth' 33 | qop_auth_int = 'auth-int' 34 | valid_qops = (qop_auth, qop_auth_int) 35 | 36 | valid_algorithms = ('MD5', 'MD5-sess') 37 | 38 | 39 | def TRACE(msg): 40 | cherrypy.log(msg, context='TOOLS.AUTH_DIGEST') 41 | 42 | # Three helper functions for users of the tool, providing three variants 43 | # of get_ha1() functions for three different kinds of credential stores. 44 | def get_ha1_dict_plain(user_password_dict): 45 | """Returns a get_ha1 function which obtains a plaintext password from a 46 | dictionary of the form: {username : password}. 47 | 48 | If you want a simple dictionary-based authentication scheme, with plaintext 49 | passwords, use get_ha1_dict_plain(my_userpass_dict) as the value for the 50 | get_ha1 argument to digest_auth(). 51 | """ 52 | def get_ha1(realm, username): 53 | password = user_password_dict.get(username) 54 | if password: 55 | return md5_hex('%s:%s:%s' % (username, realm, password)) 56 | return None 57 | 58 | return get_ha1 59 | 60 | def get_ha1_dict(user_ha1_dict): 61 | """Returns a get_ha1 function which obtains a HA1 password hash from a 62 | dictionary of the form: {username : HA1}. 63 | 64 | If you want a dictionary-based authentication scheme, but with 65 | pre-computed HA1 hashes instead of plain-text passwords, use 66 | get_ha1_dict(my_userha1_dict) as the value for the get_ha1 67 | argument to digest_auth(). 68 | """ 69 | def get_ha1(realm, username): 70 | return user_ha1_dict.get(user) 71 | 72 | return get_ha1 73 | 74 | def get_ha1_file_htdigest(filename): 75 | """Returns a get_ha1 function which obtains a HA1 password hash from a 76 | flat file with lines of the same format as that produced by the Apache 77 | htdigest utility. For example, for realm 'wonderland', username 'alice', 78 | and password '4x5istwelve', the htdigest line would be:: 79 | 80 | alice:wonderland:3238cdfe91a8b2ed8e39646921a02d4c 81 | 82 | If you want to use an Apache htdigest file as the credentials store, 83 | then use get_ha1_file_htdigest(my_htdigest_file) as the value for the 84 | get_ha1 argument to digest_auth(). It is recommended that the filename 85 | argument be an absolute path, to avoid problems. 86 | """ 87 | def get_ha1(realm, username): 88 | result = None 89 | f = open(filename, 'r') 90 | for line in f: 91 | u, r, ha1 = line.rstrip().split(':') 92 | if u == username and r == realm: 93 | result = ha1 94 | break 95 | f.close() 96 | return result 97 | 98 | return get_ha1 99 | 100 | 101 | def synthesize_nonce(s, key, timestamp=None): 102 | """Synthesize a nonce value which resists spoofing and can be checked for staleness. 103 | Returns a string suitable as the value for 'nonce' in the www-authenticate header. 104 | 105 | s 106 | A string related to the resource, such as the hostname of the server. 107 | 108 | key 109 | A secret string known only to the server. 110 | 111 | timestamp 112 | An integer seconds-since-the-epoch timestamp 113 | 114 | """ 115 | if timestamp is None: 116 | timestamp = int(time.time()) 117 | h = md5_hex('%s:%s:%s' % (timestamp, s, key)) 118 | nonce = '%s:%s' % (timestamp, h) 119 | return nonce 120 | 121 | 122 | def H(s): 123 | """The hash function H""" 124 | return md5_hex(s) 125 | 126 | 127 | class HttpDigestAuthorization (object): 128 | """Class to parse a Digest Authorization header and perform re-calculation 129 | of the digest. 130 | """ 131 | 132 | def errmsg(self, s): 133 | return 'Digest Authorization header: %s' % s 134 | 135 | def __init__(self, auth_header, http_method, debug=False): 136 | self.http_method = http_method 137 | self.debug = debug 138 | scheme, params = auth_header.split(" ", 1) 139 | self.scheme = scheme.lower() 140 | if self.scheme != 'digest': 141 | raise ValueError('Authorization scheme is not "Digest"') 142 | 143 | self.auth_header = auth_header 144 | 145 | # make a dict of the params 146 | items = parse_http_list(params) 147 | paramsd = parse_keqv_list(items) 148 | 149 | self.realm = paramsd.get('realm') 150 | self.username = paramsd.get('username') 151 | self.nonce = paramsd.get('nonce') 152 | self.uri = paramsd.get('uri') 153 | self.method = paramsd.get('method') 154 | self.response = paramsd.get('response') # the response digest 155 | self.algorithm = paramsd.get('algorithm', 'MD5') 156 | self.cnonce = paramsd.get('cnonce') 157 | self.opaque = paramsd.get('opaque') 158 | self.qop = paramsd.get('qop') # qop 159 | self.nc = paramsd.get('nc') # nonce count 160 | 161 | # perform some correctness checks 162 | if self.algorithm not in valid_algorithms: 163 | raise ValueError(self.errmsg("Unsupported value for algorithm: '%s'" % self.algorithm)) 164 | 165 | has_reqd = self.username and \ 166 | self.realm and \ 167 | self.nonce and \ 168 | self.uri and \ 169 | self.response 170 | if not has_reqd: 171 | raise ValueError(self.errmsg("Not all required parameters are present.")) 172 | 173 | if self.qop: 174 | if self.qop not in valid_qops: 175 | raise ValueError(self.errmsg("Unsupported value for qop: '%s'" % self.qop)) 176 | if not (self.cnonce and self.nc): 177 | raise ValueError(self.errmsg("If qop is sent then cnonce and nc MUST be present")) 178 | else: 179 | if self.cnonce or self.nc: 180 | raise ValueError(self.errmsg("If qop is not sent, neither cnonce nor nc can be present")) 181 | 182 | 183 | def __str__(self): 184 | return 'authorization : %s' % self.auth_header 185 | 186 | def validate_nonce(self, s, key): 187 | """Validate the nonce. 188 | Returns True if nonce was generated by synthesize_nonce() and the timestamp 189 | is not spoofed, else returns False. 190 | 191 | s 192 | A string related to the resource, such as the hostname of the server. 193 | 194 | key 195 | A secret string known only to the server. 196 | 197 | Both s and key must be the same values which were used to synthesize the nonce 198 | we are trying to validate. 199 | """ 200 | try: 201 | timestamp, hashpart = self.nonce.split(':', 1) 202 | s_timestamp, s_hashpart = synthesize_nonce(s, key, timestamp).split(':', 1) 203 | is_valid = s_hashpart == hashpart 204 | if self.debug: 205 | TRACE('validate_nonce: %s' % is_valid) 206 | return is_valid 207 | except ValueError: # split() error 208 | pass 209 | return False 210 | 211 | 212 | def is_nonce_stale(self, max_age_seconds=600): 213 | """Returns True if a validated nonce is stale. The nonce contains a 214 | timestamp in plaintext and also a secure hash of the timestamp. You should 215 | first validate the nonce to ensure the plaintext timestamp is not spoofed. 216 | """ 217 | try: 218 | timestamp, hashpart = self.nonce.split(':', 1) 219 | if int(timestamp) + max_age_seconds > int(time.time()): 220 | return False 221 | except ValueError: # int() error 222 | pass 223 | if self.debug: 224 | TRACE("nonce is stale") 225 | return True 226 | 227 | 228 | def HA2(self, entity_body=''): 229 | """Returns the H(A2) string. See :rfc:`2617` section 3.2.2.3.""" 230 | # RFC 2617 3.2.2.3 231 | # If the "qop" directive's value is "auth" or is unspecified, then A2 is: 232 | # A2 = method ":" digest-uri-value 233 | # 234 | # If the "qop" value is "auth-int", then A2 is: 235 | # A2 = method ":" digest-uri-value ":" H(entity-body) 236 | if self.qop is None or self.qop == "auth": 237 | a2 = '%s:%s' % (self.http_method, self.uri) 238 | elif self.qop == "auth-int": 239 | a2 = "%s:%s:%s" % (self.http_method, self.uri, H(entity_body)) 240 | else: 241 | # in theory, this should never happen, since I validate qop in __init__() 242 | raise ValueError(self.errmsg("Unrecognized value for qop!")) 243 | return H(a2) 244 | 245 | 246 | def request_digest(self, ha1, entity_body=''): 247 | """Calculates the Request-Digest. See :rfc:`2617` section 3.2.2.1. 248 | 249 | ha1 250 | The HA1 string obtained from the credentials store. 251 | 252 | entity_body 253 | If 'qop' is set to 'auth-int', then A2 includes a hash 254 | of the "entity body". The entity body is the part of the 255 | message which follows the HTTP headers. See :rfc:`2617` section 256 | 4.3. This refers to the entity the user agent sent in the request which 257 | has the Authorization header. Typically GET requests don't have an entity, 258 | and POST requests do. 259 | 260 | """ 261 | ha2 = self.HA2(entity_body) 262 | # Request-Digest -- RFC 2617 3.2.2.1 263 | if self.qop: 264 | req = "%s:%s:%s:%s:%s" % (self.nonce, self.nc, self.cnonce, self.qop, ha2) 265 | else: 266 | req = "%s:%s" % (self.nonce, ha2) 267 | 268 | # RFC 2617 3.2.2.2 269 | # 270 | # If the "algorithm" directive's value is "MD5" or is unspecified, then A1 is: 271 | # A1 = unq(username-value) ":" unq(realm-value) ":" passwd 272 | # 273 | # If the "algorithm" directive's value is "MD5-sess", then A1 is 274 | # calculated only once - on the first request by the client following 275 | # receipt of a WWW-Authenticate challenge from the server. 276 | # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd ) 277 | # ":" unq(nonce-value) ":" unq(cnonce-value) 278 | if self.algorithm == 'MD5-sess': 279 | ha1 = H('%s:%s:%s' % (ha1, self.nonce, self.cnonce)) 280 | 281 | digest = H('%s:%s' % (ha1, req)) 282 | return digest 283 | 284 | 285 | 286 | def www_authenticate(realm, key, algorithm='MD5', nonce=None, qop=qop_auth, stale=False): 287 | """Constructs a WWW-Authenticate header for Digest authentication.""" 288 | if qop not in valid_qops: 289 | raise ValueError("Unsupported value for qop: '%s'" % qop) 290 | if algorithm not in valid_algorithms: 291 | raise ValueError("Unsupported value for algorithm: '%s'" % algorithm) 292 | 293 | if nonce is None: 294 | nonce = synthesize_nonce(realm, key) 295 | s = 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"' % ( 296 | realm, nonce, algorithm, qop) 297 | if stale: 298 | s += ', stale="true"' 299 | return s 300 | 301 | 302 | def digest_auth(realm, get_ha1, key, debug=False): 303 | """A CherryPy tool which hooks at before_handler to perform 304 | HTTP Digest Access Authentication, as specified in :rfc:`2617`. 305 | 306 | If the request has an 'authorization' header with a 'Digest' scheme, this 307 | tool authenticates the credentials supplied in that header. If 308 | the request has no 'authorization' header, or if it does but the scheme is 309 | not "Digest", or if authentication fails, the tool sends a 401 response with 310 | a 'WWW-Authenticate' Digest header. 311 | 312 | realm 313 | A string containing the authentication realm. 314 | 315 | get_ha1 316 | A callable which looks up a username in a credentials store 317 | and returns the HA1 string, which is defined in the RFC to be 318 | MD5(username : realm : password). The function's signature is: 319 | ``get_ha1(realm, username)`` 320 | where username is obtained from the request's 'authorization' header. 321 | If username is not found in the credentials store, get_ha1() returns 322 | None. 323 | 324 | key 325 | A secret string known only to the server, used in the synthesis of nonces. 326 | 327 | """ 328 | request = cherrypy.serving.request 329 | 330 | auth_header = request.headers.get('authorization') 331 | nonce_is_stale = False 332 | if auth_header is not None: 333 | try: 334 | auth = HttpDigestAuthorization(auth_header, request.method, debug=debug) 335 | except ValueError: 336 | raise cherrypy.HTTPError(400, "The Authorization header could not be parsed.") 337 | 338 | if debug: 339 | TRACE(str(auth)) 340 | 341 | if auth.validate_nonce(realm, key): 342 | ha1 = get_ha1(realm, auth.username) 343 | if ha1 is not None: 344 | # note that for request.body to be available we need to hook in at 345 | # before_handler, not on_start_resource like 3.1.x digest_auth does. 346 | digest = auth.request_digest(ha1, entity_body=request.body) 347 | if digest == auth.response: # authenticated 348 | if debug: 349 | TRACE("digest matches auth.response") 350 | # Now check if nonce is stale. 351 | # The choice of ten minutes' lifetime for nonce is somewhat arbitrary 352 | nonce_is_stale = auth.is_nonce_stale(max_age_seconds=600) 353 | if not nonce_is_stale: 354 | request.login = auth.username 355 | if debug: 356 | TRACE("authentication of %s successful" % auth.username) 357 | return 358 | 359 | # Respond with 401 status and a WWW-Authenticate header 360 | header = www_authenticate(realm, key, stale=nonce_is_stale) 361 | if debug: 362 | TRACE(header) 363 | cherrypy.serving.response.headers['WWW-Authenticate'] = header 364 | raise cherrypy.HTTPError(401, "You are not authorized to access that resource") 365 | 366 | -------------------------------------------------------------------------------- /cherrypy/_cpchecker.py: -------------------------------------------------------------------------------- 1 | import os 2 | import warnings 3 | 4 | import cherrypy 5 | from cherrypy._cpcompat import iteritems, copykeys, builtins 6 | 7 | 8 | class Checker(object): 9 | """A checker for CherryPy sites and their mounted applications. 10 | 11 | When this object is called at engine startup, it executes each 12 | of its own methods whose names start with ``check_``. If you wish 13 | to disable selected checks, simply add a line in your global 14 | config which sets the appropriate method to False:: 15 | 16 | [global] 17 | checker.check_skipped_app_config = False 18 | 19 | You may also dynamically add or replace ``check_*`` methods in this way. 20 | """ 21 | 22 | on = True 23 | """If True (the default), run all checks; if False, turn off all checks.""" 24 | 25 | 26 | def __init__(self): 27 | self._populate_known_types() 28 | 29 | def __call__(self): 30 | """Run all check_* methods.""" 31 | if self.on: 32 | oldformatwarning = warnings.formatwarning 33 | warnings.formatwarning = self.formatwarning 34 | try: 35 | for name in dir(self): 36 | if name.startswith("check_"): 37 | method = getattr(self, name) 38 | if method and hasattr(method, '__call__'): 39 | method() 40 | finally: 41 | warnings.formatwarning = oldformatwarning 42 | 43 | def formatwarning(self, message, category, filename, lineno, line=None): 44 | """Function to format a warning.""" 45 | return "CherryPy Checker:\n%s\n\n" % message 46 | 47 | # This value should be set inside _cpconfig. 48 | global_config_contained_paths = False 49 | 50 | def check_app_config_entries_dont_start_with_script_name(self): 51 | """Check for Application config with sections that repeat script_name.""" 52 | for sn, app in cherrypy.tree.apps.items(): 53 | if not isinstance(app, cherrypy.Application): 54 | continue 55 | if not app.config: 56 | continue 57 | if sn == '': 58 | continue 59 | sn_atoms = sn.strip("/").split("/") 60 | for key in app.config.keys(): 61 | key_atoms = key.strip("/").split("/") 62 | if key_atoms[:len(sn_atoms)] == sn_atoms: 63 | warnings.warn( 64 | "The application mounted at %r has config " \ 65 | "entries that start with its script name: %r" % (sn, key)) 66 | 67 | def check_site_config_entries_in_app_config(self): 68 | """Check for mounted Applications that have site-scoped config.""" 69 | for sn, app in iteritems(cherrypy.tree.apps): 70 | if not isinstance(app, cherrypy.Application): 71 | continue 72 | 73 | msg = [] 74 | for section, entries in iteritems(app.config): 75 | if section.startswith('/'): 76 | for key, value in iteritems(entries): 77 | for n in ("engine.", "server.", "tree.", "checker."): 78 | if key.startswith(n): 79 | msg.append("[%s] %s = %s" % (section, key, value)) 80 | if msg: 81 | msg.insert(0, 82 | "The application mounted at %r contains the following " 83 | "config entries, which are only allowed in site-wide " 84 | "config. Move them to a [global] section and pass them " 85 | "to cherrypy.config.update() instead of tree.mount()." % sn) 86 | warnings.warn(os.linesep.join(msg)) 87 | 88 | def check_skipped_app_config(self): 89 | """Check for mounted Applications that have no config.""" 90 | for sn, app in cherrypy.tree.apps.items(): 91 | if not isinstance(app, cherrypy.Application): 92 | continue 93 | if not app.config: 94 | msg = "The Application mounted at %r has an empty config." % sn 95 | if self.global_config_contained_paths: 96 | msg += (" It looks like the config you passed to " 97 | "cherrypy.config.update() contains application-" 98 | "specific sections. You must explicitly pass " 99 | "application config via " 100 | "cherrypy.tree.mount(..., config=app_config)") 101 | warnings.warn(msg) 102 | return 103 | 104 | def check_app_config_brackets(self): 105 | """Check for Application config with extraneous brackets in section names.""" 106 | for sn, app in cherrypy.tree.apps.items(): 107 | if not isinstance(app, cherrypy.Application): 108 | continue 109 | if not app.config: 110 | continue 111 | for key in app.config.keys(): 112 | if key.startswith("[") or key.endswith("]"): 113 | warnings.warn( 114 | "The application mounted at %r has config " \ 115 | "section names with extraneous brackets: %r. " 116 | "Config *files* need brackets; config *dicts* " 117 | "(e.g. passed to tree.mount) do not." % (sn, key)) 118 | 119 | def check_static_paths(self): 120 | """Check Application config for incorrect static paths.""" 121 | # Use the dummy Request object in the main thread. 122 | request = cherrypy.request 123 | for sn, app in cherrypy.tree.apps.items(): 124 | if not isinstance(app, cherrypy.Application): 125 | continue 126 | request.app = app 127 | for section in app.config: 128 | # get_resource will populate request.config 129 | request.get_resource(section + "/dummy.html") 130 | conf = request.config.get 131 | 132 | if conf("tools.staticdir.on", False): 133 | msg = "" 134 | root = conf("tools.staticdir.root") 135 | dir = conf("tools.staticdir.dir") 136 | if dir is None: 137 | msg = "tools.staticdir.dir is not set." 138 | else: 139 | fulldir = "" 140 | if os.path.isabs(dir): 141 | fulldir = dir 142 | if root: 143 | msg = ("dir is an absolute path, even " 144 | "though a root is provided.") 145 | testdir = os.path.join(root, dir[1:]) 146 | if os.path.exists(testdir): 147 | msg += ("\nIf you meant to serve the " 148 | "filesystem folder at %r, remove " 149 | "the leading slash from dir." % testdir) 150 | else: 151 | if not root: 152 | msg = "dir is a relative path and no root provided." 153 | else: 154 | fulldir = os.path.join(root, dir) 155 | if not os.path.isabs(fulldir): 156 | msg = "%r is not an absolute path." % fulldir 157 | 158 | if fulldir and not os.path.exists(fulldir): 159 | if msg: 160 | msg += "\n" 161 | msg += ("%r (root + dir) is not an existing " 162 | "filesystem path." % fulldir) 163 | 164 | if msg: 165 | warnings.warn("%s\nsection: [%s]\nroot: %r\ndir: %r" 166 | % (msg, section, root, dir)) 167 | 168 | 169 | # -------------------------- Compatibility -------------------------- # 170 | 171 | obsolete = { 172 | 'server.default_content_type': 'tools.response_headers.headers', 173 | 'log_access_file': 'log.access_file', 174 | 'log_config_options': None, 175 | 'log_file': 'log.error_file', 176 | 'log_file_not_found': None, 177 | 'log_request_headers': 'tools.log_headers.on', 178 | 'log_to_screen': 'log.screen', 179 | 'show_tracebacks': 'request.show_tracebacks', 180 | 'throw_errors': 'request.throw_errors', 181 | 'profiler.on': ('cherrypy.tree.mount(profiler.make_app(' 182 | 'cherrypy.Application(Root())))'), 183 | } 184 | 185 | deprecated = {} 186 | 187 | def _compat(self, config): 188 | """Process config and warn on each obsolete or deprecated entry.""" 189 | for section, conf in config.items(): 190 | if isinstance(conf, dict): 191 | for k, v in conf.items(): 192 | if k in self.obsolete: 193 | warnings.warn("%r is obsolete. Use %r instead.\n" 194 | "section: [%s]" % 195 | (k, self.obsolete[k], section)) 196 | elif k in self.deprecated: 197 | warnings.warn("%r is deprecated. Use %r instead.\n" 198 | "section: [%s]" % 199 | (k, self.deprecated[k], section)) 200 | else: 201 | if section in self.obsolete: 202 | warnings.warn("%r is obsolete. Use %r instead." 203 | % (section, self.obsolete[section])) 204 | elif section in self.deprecated: 205 | warnings.warn("%r is deprecated. Use %r instead." 206 | % (section, self.deprecated[section])) 207 | 208 | def check_compatibility(self): 209 | """Process config and warn on each obsolete or deprecated entry.""" 210 | self._compat(cherrypy.config) 211 | for sn, app in cherrypy.tree.apps.items(): 212 | if not isinstance(app, cherrypy.Application): 213 | continue 214 | self._compat(app.config) 215 | 216 | 217 | # ------------------------ Known Namespaces ------------------------ # 218 | 219 | extra_config_namespaces = [] 220 | 221 | def _known_ns(self, app): 222 | ns = ["wsgi"] 223 | ns.extend(copykeys(app.toolboxes)) 224 | ns.extend(copykeys(app.namespaces)) 225 | ns.extend(copykeys(app.request_class.namespaces)) 226 | ns.extend(copykeys(cherrypy.config.namespaces)) 227 | ns += self.extra_config_namespaces 228 | 229 | for section, conf in app.config.items(): 230 | is_path_section = section.startswith("/") 231 | if is_path_section and isinstance(conf, dict): 232 | for k, v in conf.items(): 233 | atoms = k.split(".") 234 | if len(atoms) > 1: 235 | if atoms[0] not in ns: 236 | # Spit out a special warning if a known 237 | # namespace is preceded by "cherrypy." 238 | if (atoms[0] == "cherrypy" and atoms[1] in ns): 239 | msg = ("The config entry %r is invalid; " 240 | "try %r instead.\nsection: [%s]" 241 | % (k, ".".join(atoms[1:]), section)) 242 | else: 243 | msg = ("The config entry %r is invalid, because " 244 | "the %r config namespace is unknown.\n" 245 | "section: [%s]" % (k, atoms[0], section)) 246 | warnings.warn(msg) 247 | elif atoms[0] == "tools": 248 | if atoms[1] not in dir(cherrypy.tools): 249 | msg = ("The config entry %r may be invalid, " 250 | "because the %r tool was not found.\n" 251 | "section: [%s]" % (k, atoms[1], section)) 252 | warnings.warn(msg) 253 | 254 | def check_config_namespaces(self): 255 | """Process config and warn on each unknown config namespace.""" 256 | for sn, app in cherrypy.tree.apps.items(): 257 | if not isinstance(app, cherrypy.Application): 258 | continue 259 | self._known_ns(app) 260 | 261 | 262 | 263 | 264 | # -------------------------- Config Types -------------------------- # 265 | 266 | known_config_types = {} 267 | 268 | def _populate_known_types(self): 269 | b = [x for x in vars(builtins).values() 270 | if type(x) is type(str)] 271 | 272 | def traverse(obj, namespace): 273 | for name in dir(obj): 274 | # Hack for 3.2's warning about body_params 275 | if name == 'body_params': 276 | continue 277 | vtype = type(getattr(obj, name, None)) 278 | if vtype in b: 279 | self.known_config_types[namespace + "." + name] = vtype 280 | 281 | traverse(cherrypy.request, "request") 282 | traverse(cherrypy.response, "response") 283 | traverse(cherrypy.server, "server") 284 | traverse(cherrypy.engine, "engine") 285 | traverse(cherrypy.log, "log") 286 | 287 | def _known_types(self, config): 288 | msg = ("The config entry %r in section %r is of type %r, " 289 | "which does not match the expected type %r.") 290 | 291 | for section, conf in config.items(): 292 | if isinstance(conf, dict): 293 | for k, v in conf.items(): 294 | if v is not None: 295 | expected_type = self.known_config_types.get(k, None) 296 | vtype = type(v) 297 | if expected_type and vtype != expected_type: 298 | warnings.warn(msg % (k, section, vtype.__name__, 299 | expected_type.__name__)) 300 | else: 301 | k, v = section, conf 302 | if v is not None: 303 | expected_type = self.known_config_types.get(k, None) 304 | vtype = type(v) 305 | if expected_type and vtype != expected_type: 306 | warnings.warn(msg % (k, section, vtype.__name__, 307 | expected_type.__name__)) 308 | 309 | def check_config_types(self): 310 | """Assert that config values are of the same type as default values.""" 311 | self._known_types(cherrypy.config) 312 | for sn, app in cherrypy.tree.apps.items(): 313 | if not isinstance(app, cherrypy.Application): 314 | continue 315 | self._known_types(app.config) 316 | 317 | 318 | # -------------------- Specific config warnings -------------------- # 319 | 320 | def check_localhost(self): 321 | """Warn if any socket_host is 'localhost'. See #711.""" 322 | for k, v in cherrypy.config.items(): 323 | if k == 'server.socket_host' and v == 'localhost': 324 | warnings.warn("The use of 'localhost' as a socket host can " 325 | "cause problems on newer systems, since 'localhost' can " 326 | "map to either an IPv4 or an IPv6 address. You should " 327 | "use '127.0.0.1' or '[::1]' instead.") 328 | --------------------------------------------------------------------------------