', "")
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 |
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 |
129 |
"""
130 |
131 | TEMPLATE_FRAMESET = """
132 | CherryPy coverage data
133 |
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 |
--------------------------------------------------------------------------------