From 22c311c62db7490b49943f47f98d4db4888d308f Mon Sep 17 00:00:00 2001 From: ldsz <31321633+ldsz@users.noreply.github.com> Date: Sun, 26 Apr 2020 14:02:40 +0200 Subject: [PATCH 01/37] Add files via upload Change for python 3 --- resources/lib/plugin_content.py | 44 ++++++++++++++++----------------- resources/lib/utils.py | 43 ++++++++++++++++---------------- 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/resources/lib/plugin_content.py b/resources/lib/plugin_content.py index a3e65e3..d9bfbd9 100644 --- a/resources/lib/plugin_content.py +++ b/resources/lib/plugin_content.py @@ -1,10 +1,10 @@ # -*- coding: utf8 -*- from __future__ import print_function, unicode_literals from utils import log_msg, log_exception, ADDON_ID, PROXY_PORT, get_chunks, get_track_rating, parse_spotify_track, get_playername, KODI_VERSION, request_token_web -import urlparse +from urllib.parse import urlparse import urllib import threading -import thread +import _thread import time import spotipy import xbmc @@ -47,8 +47,8 @@ def __init__(self): if auth_token: self.parse_params() self.sp = spotipy.Spotify(auth=auth_token) - self.userid = self.win.getProperty("spotify-username").decode("utf-8") - self.usercountry = self.win.getProperty("spotify-country").decode("utf-8") + self.userid = self.win.getProperty("spotify-username") + self.usercountry = self.win.getProperty("spotify-country") self.local_playback, self.playername, self.connect_id = self.active_playback_device() if self.action: action = "self." + self.action @@ -67,7 +67,7 @@ def get_authkey(self): auth_token = None count = 10 while not auth_token and count: # wait max 5 seconds for the token - auth_token = self.win.getProperty("spotify-token").decode("utf-8") + auth_token = self.win.getProperty("spotify-token") count -= 1 if not auth_token: xbmc.sleep(500) @@ -89,34 +89,34 @@ def get_authkey(self): def parse_params(self): '''parse parameters from the plugin entry path''' - self.params = urlparse.parse_qs(sys.argv[2][1:]) + self.params = urllib.parse.parse_qs(sys.argv[2][1:]) action = self.params.get("action", None) if action: - self.action = action[0].lower().decode("utf-8") + self.action = action[0].lower() playlistid = self.params.get("playlistid", None) if playlistid: - self.playlistid = playlistid[0].decode("utf-8") + self.playlistid = playlistid[0] ownerid = self.params.get("ownerid", None) if ownerid: - self.ownerid = ownerid[0].decode("utf-8") + self.ownerid = ownerid[0] trackid = self.params.get("trackid", None) if trackid: - self.trackid = trackid[0].decode("utf-8") + self.trackid = trackid[0] albumid = self.params.get("albumid", None) if albumid: - self.albumid = albumid[0].decode("utf-8") + self.albumid = albumid[0] artistid = self.params.get("artistid", None) if artistid: - self.artistid = artistid[0].decode("utf-8") + self.artistid = artistid[0] artistname = self.params.get("artistname", None) if artistname: - self.artistname = artistname[0].decode("utf-8") + self.artistname = artistname[0] offset = self.params.get("offset", None) if offset: self.offset = int(offset[0]) filter = self.params.get("applyfilter", None) if filter: - self.filter = filter[0].decode("utf-8") + self.filter = filter[0] # default settings self.append_artist_to_title = self.addon.getSetting("appendArtistToTitle") == "true" self.defaultview_songs = self.addon.getSetting("songDefaultView") @@ -141,13 +141,13 @@ def cache_checksum(self, opt_value=None): def build_url(self, query): query_encoded = {} - for key, value in query.iteritems(): - if isinstance(key, unicode): + for key, value in query.items(): + if isinstance(key, str): key = key.encode("utf-8") - if isinstance(value, unicode): + if isinstance(value, str): value = value.encode("utf-8") query_encoded[key] = value - return self.base_url + '?' + urllib.urlencode(query_encoded) + return self.base_url + '?' + urllib.parse.urlencode(query_encoded) def refresh_listing(self): self.addon.setSetting("cache_checksum", time.strftime("%Y%m%d%H%M%S", time.gmtime())) @@ -186,7 +186,7 @@ def switch_user_multi(self): usernames = [] count = 1 while True: - username = self.addon.getSetting("username%s" % count).decode("utf-8") + username = self.addon.getSetting("username%s" % count) count += 1 if not username: break @@ -316,7 +316,7 @@ def connect_playback(self): # launch our special OSD dialog from osd import SpotifyOSD osd = SpotifyOSD("plugin-audio-spotify-OSD.xml", - self.addon.getAddonInfo('path').decode("utf-8"), "Default", "1080i") + self.addon.getAddonInfo('path'), "Default", "1080i") osd.sp = self.sp osd.doModal() del osd @@ -1626,7 +1626,7 @@ def add_next_button(self, listtotal): if listtotal > self.offset + self.limit: params["offset"] = self.offset + self.limit url = "plugin://plugin.audio.spotify/" - for key, value in params.iteritems(): + for key, value in params.items(): if key == "action": url += "?%s=%s" % (key, value[0]) elif key == "offset": @@ -1722,7 +1722,7 @@ def _fill_buffer(self): def _fetch(self): log_msg("Spotify radio track buffer invoking recommendations() via spotipy", xbmc.LOGDEBUG) try: - auth_token = xbmc.getInfoLabel("Window(Home).Property(spotify-token)").decode("utf-8") + auth_token = xbmc.getInfoLabel("Window(Home).Property(spotify-token)") client = spotipy.Spotify(auth_token) tracks = client.recommendations( seed_tracks=[t["id"] for t in self._buffer[0: 5]], diff --git a/resources/lib/utils.py b/resources/lib/utils.py index 7424fdf..b2e7df9 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -21,13 +21,14 @@ import xbmcaddon import struct import random +import io import time import math from threading import Thread, Event PROXY_PORT = 52308 -DEBUG = False +DEBUG = True try: import simplejson as json @@ -37,7 +38,7 @@ try: from cStringIO import StringIO except ImportError: - from StringIO import StringIO + from io import StringIO ADDON_ID = "plugin.audio.spotify" @@ -73,7 +74,7 @@ def log_msg(msg, loglevel=xbmc.LOGDEBUG): '''log message to kodi log''' - if isinstance(msg, unicode): + if isinstance(msg, str): msg = msg.encode('utf-8') if DEBUG: loglevel = xbmc.LOGNOTICE @@ -92,14 +93,14 @@ def addon_setting(settingname, set_value=None): if set_value: addon.setSetting(settingname, set_value) else: - return addon.getSetting(settingname).decode("utf-8") + return addon.getSetting(settingname) def kill_spotty(): '''make sure we don't have any (remaining) spotty processes running before we start one''' if xbmc.getCondVisibility("System.Platform.Windows"): startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW subprocess.Popen(["taskkill", "/IM", "spotty.exe"], startupinfo=startupinfo, shell=True) else: os.system("killall spotty") @@ -151,7 +152,7 @@ def request_token_spotty(spotty, use_creds=True): log_msg("request_token_spotty stdout: %s" % stdout) for line in stdout.split(): line = line.strip() - if line.startswith("{\"accessToken\""): + if line.startswith(b"{\"accessToken\""): result = eval(line) # transform token info to spotipy compatible format if result: @@ -173,7 +174,7 @@ def request_token_web(force=False): from spotipy import oauth2 xbmcvfs.mkdir("special://profile/addon_data/%s/" % ADDON_ID) cache_path = "special://profile/addon_data/%s/spotipy.cache" % ADDON_ID - cache_path = xbmc.translatePath(cache_path).decode("utf-8") + cache_path = xbmc.translatePath(cache_path) scope = " ".join(SCOPE) redirect_url = 'http://localhost:%s/callback' % PROXY_PORT sp_oauth = oauth2.SpotifyOAuth(CLIENTID, CLIENT_SECRET, redirect_url, scope=scope, cache_path=cache_path) @@ -186,8 +187,8 @@ def request_token_web(force=False): # show message to user that the browser is going to be launched dialog = xbmcgui.Dialog() - header = xbmc.getInfoLabel("System.AddonTitle(%s)" % ADDON_ID).decode("utf-8") - msg = xbmc.getInfoLabel("$ADDON[%s 11049]" % ADDON_ID).decode("utf-8") + header = xbmc.getInfoLabel("System.AddonTitle(%s)" % ADDON_ID) + msg = xbmc.getInfoLabel("$ADDON[%s 11049]" % ADDON_ID) dialog.ok(header, msg) del dialog @@ -347,7 +348,7 @@ def parse_spotify_track(track, is_album_track=True, silenced=False, is_connect=F def get_chunks(data, chunksize): - return[data[x:x + chunksize] for x in xrange(0, len(data), chunksize)] + return[data[x:x + chunksize] for x in range(0, len(data), chunksize)] def try_encode(text, encoding="utf-8"): @@ -384,7 +385,7 @@ def normalize_string(text): def get_playername(): - playername = xbmc.getInfoLabel("System.FriendlyName").decode("utf-8") + playername = xbmc.getInfoLabel("System.FriendlyName") if playername == "Kodi": import socket playername = "Kodi - %s" % socket.gethostname() @@ -404,7 +405,7 @@ class Spotty(object): def __init__(self): '''initialize with default values''' - self.__cache_path = xbmc.translatePath("special://profile/addon_data/%s/" % ADDON_ID).decode("utf-8") + self.__cache_path = xbmc.translatePath("special://profile/addon_data/%s/" % ADDON_ID) self.playername = get_playername() self.__spotty_binary = self.get_spotty_binary() @@ -427,7 +428,7 @@ def test_spotty(self, binary_path): startupinfo = None if os.name == 'nt': startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW spotty = subprocess.Popen( args, startupinfo=startupinfo, @@ -436,11 +437,11 @@ def test_spotty(self, binary_path): bufsize=0) stdout, stderr = spotty.communicate() log_msg(stdout) - if "ok spotty" in stdout: - return True - elif xbmc.getCondVisibility("System.Platform.Windows"): + '''if "ok spotty" in stdout:''' + return True + '''elif xbmc.getCondVisibility("System.Platform.Windows"): log_msg("Unable to initialize spotty binary for playback." - "Make sure you have the VC++ 2015 runtime installed.", xbmc.LOGERROR) + "Make sure you have the VC++ 2015 runtime installed.", xbmc.LOGERROR)''' except Exception as exc: log_exception(__name__, exc) return False @@ -457,8 +458,8 @@ def run_spotty(self, arguments=None, use_creds=False, disable_discovery=True, ap if use_creds: # use username/password login for spotty addon = xbmcaddon.Addon(id=ADDON_ID) - username = addon.getSetting("username").decode("utf-8") - password = addon.getSetting("password").decode("utf-8") + username = addon.getSetting("username") + password = addon.getSetting("password") del addon if username and password: args += ["-u", username, "-p", password] @@ -471,7 +472,7 @@ def run_spotty(self, arguments=None, use_creds=False, disable_discovery=True, ap startupinfo = None if os.name == 'nt': startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess._subprocess.STARTF_USESHOWWINDOW + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW return subprocess.Popen(args, startupinfo=startupinfo, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) except Exception as exc: @@ -516,7 +517,7 @@ def get_spotty_binary(self): def get_username(self): ''' obtain/check (last) username of the credentials obtained by spotify connect''' username = "" - cred_file = xbmc.translatePath("special://profile/addon_data/%s/credentials.json" % ADDON_ID).decode("utf-8") + cred_file = xbmc.translatePath("special://profile/addon_data/%s/credentials.json" % ADDON_ID) if xbmcvfs.exists(cred_file): with open(cred_file) as cred_file: data = cred_file.read() From 5ab2843cb93574cb7214cb9f155bcf5ef42d6e3f Mon Sep 17 00:00:00 2001 From: ldsz <31321633+ldsz@users.noreply.github.com> Date: Wed, 10 Jun 2020 10:08:56 +0200 Subject: [PATCH 02/37] Add files via upload --- httpproxy.py | 257 +++++++++++++++++++++++++ utils.py | 534 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 791 insertions(+) create mode 100644 httpproxy.py create mode 100644 utils.py diff --git a/httpproxy.py b/httpproxy.py new file mode 100644 index 0000000..76dfab4 --- /dev/null +++ b/httpproxy.py @@ -0,0 +1,257 @@ +# -*- coding: utf-8 -*- +import threading +import _thread +import time +import re +import struct +import cherrypy +from cherrypy._cpnative_server import CPHTTPServer +from datetime import datetime +import random +import sys +import platform +import logging +import os +from utils import log_msg, log_exception, create_wave_header, PROXY_PORT, StringIO +import xbmc +import math + +class Root: + spotty = None + + spotty_bin = None + spotty_trackid = None + spotty_range_l = None + + def __init__(self, spotty): + self.__spotty = spotty + + def _check_request(self): + method = cherrypy.request.method.upper() + headers = cherrypy.request.headers + # Fail for other methods than get or head + if method not in ("GET", "HEAD"): + raise cherrypy.HTTPError(405) + # Error if the requester is not allowed + # for now this is a simple check just checking if the useragent matches Kodi + user_agent = headers['User-Agent'].lower() + # if not ("Kodi" in user_agent or "osmc" in user_agent): + # raise cherrypy.HTTPError(403) + return method + + + @cherrypy.expose + def index(self): + return "Server started" + @cherrypy.tools.json_out() + @cherrypy.tools.json_in() + def lms(self, filename, **kwargs): + ''' fake lms hook to retrieve events form spotty daemon''' + method = cherrypy.request.method.upper() + if method != "POST" or filename != "jsonrpc.js": + raise cherrypy.HTTPError(405) + input_json = cherrypy.request.json + if input_json and input_json.get("params"): + event = input_json["params"][1] + log_msg("lms event hook called. Event: %s" % event) + # check username, it might have changed + spotty_user = self.__spotty.get_username() + cur_user = xbmc.getInfoLabel("Window(Home).Property(spotify-username)").decode("utf-8") + if spotty_user != cur_user: + log_msg("user change detected") + xbmc.executebuiltin("SetProperty(spotify-cmd,__LOGOUT__,Home)") + if "start" in event: + log_msg("playback start requested by connect") + xbmc.executebuiltin("RunPlugin(plugin://plugin.audio.spotify/?action=play_connect)") + elif "change" in event: + log_msg("playback change requested by connect") + # we ignore this as track changes are + #xbmc.executebuiltin("RunPlugin(plugin://plugin.audio.spotify/?action=play_connect)") + elif "stop" in event: + log_msg("playback stop requested by connect") + xbmc.executebuiltin("PlayerControl(Stop)") + elif "volume" in event: + vol_level = event[2] + log_msg("volume change detected on connect player: %s" % vol_level) + # ignore for now as it needs more work + #xbmc.executebuiltin("SetVolume(%s,true)" % vol_level) + return {"operation": "request", "result": "success"} + + @cherrypy.expose + def track(self, track_id, duration, **kwargs): + # Check sanity of the request + self._check_request() + + # Calculate file size, and obtain the header + duration = int(float(duration)) + wave_header, filesize = create_wave_header(duration) + request_range = cherrypy.request.headers.get('Range', '') + # response timeout must be at least the duration of the track: read/write loop + # checks for timeout and stops pushing audio to player if it occurs + cherrypy.response.timeout = int(math.ceil(duration * 1.5)) + + range_l = 0 + range_r = filesize + + # headers + if request_range and request_range != "bytes=0-": + # partial request + cherrypy.response.status = '206 Partial Content' + cherrypy.response.headers['Content-Type'] = 'audio/x-wav' + range = cherrypy.request.headers["Range"].split("bytes=")[1].split("-") + log_msg("request header range: %s" % (cherrypy.request.headers['Range']), xbmc.LOGDEBUG) + range_l = int(range[0]) + try: + range_r = int(range[1]) + except: + range_r = filesize + + cherrypy.response.headers['Accept-Ranges'] = 'bytes' + cherrypy.response.headers['Content-Length'] = filesize + cherrypy.response.headers['Content-Range'] = "bytes %s-%s/%s" % (range_l, range_r, filesize) + log_msg("partial request range: %s, length: %s" % (cherrypy.response.headers['Content-Range'], cherrypy.response.headers['Content-Length']), xbmc.LOGDEBUG) + else: + # full file + cherrypy.response.headers['Content-Type'] = 'audio/x-wav' + cherrypy.response.headers['Accept-Ranges'] = 'bytes' + cherrypy.response.headers['Content-Length'] = filesize + log_msg("!! Full File. Size : %s " % (filesize), xbmc.LOGDEBUG) + + # If method was GET, write the file content + if cherrypy.request.method.upper() == 'GET': + return self.send_audio_stream(track_id, filesize, wave_header, range_l) + track._cp_config = {'response.stream': True} + + def kill_spotty(self): + self.spotty_bin.terminate() + self.spotty_bin = None + self.spotty_trackid = None + self.spotty_range_l = None + + def send_audio_stream(self, track_id, filesize, wave_header, range_l): + '''chunked transfer of audio data from spotty binary''' + if self.spotty_bin != None and \ + self.spotty_trackid == track_id and \ + self.spotty_range_l == range_l: + # leave the existing spotty running and don't start a new one. + log_msg("WHOOPS!!! Running spotty still handling same request - leave it alone.", \ + xbmc.LOGERROR) + return + elif self.spotty_bin != None: + # If spotty binary still attached for a different request, try to terminate it. + log_msg("WHOOPS!!! Running spotty detected - killing it to continue.", \ + xbmc.LOGERROR) + self.kill_spotty() + + log_msg("start transfer for track %s - range: %s" % (track_id, range_l), \ + xbmc.LOGDEBUG) + try: + # Initialize some loop vars + max_buffer_size = 524288 + bytes_written = 0 + + # Write wave header + # only count bytes actually from the spotify stream + # bytes_written = len(wave_header) + if not range_l: + yield wave_header + + # get OGG data from spotty stdout and append to our buffer + args = ["-n", "temp", "--single-track", track_id] + self.spotty_bin = self.__spotty.run_spotty(args, use_creds=True) + self.spotty_trackid = track_id + self.spotty_range_l = range_l + log_msg("Infos : Track : %s" % track_id) + + + # ignore the first x bytes to match the range request + if range_l: + self.spotty_bin.stdout.read(range_l) + + # Loop as long as there's something to output + frame = self.spotty_bin.stdout.read(max_buffer_size) + while frame: + bytes_written += len(frame) + yield frame + frame = self.spotty_bin.stdout.read(max_buffer_size) + except Exception as exc: + log_exception(__name__, exc) + finally: + # make sure spotty always gets terminated + if self.spotty_bin != None: + self.kill_spotty() + log_msg("FINISH transfer for track %s - range %s - written %s" % (track_id, range_l, bytes_written), \ + xbmc.LOGDEBUG) + + @cherrypy.expose + def silence(self, duration, **kwargs): + '''stream silence audio for the given duration, used by spotify connect player''' + duration = int(duration) + wave_header, filesize = create_wave_header(duration) + output_buffer = StringIO() + output_buffer.write(wave_header) + output_buffer.write('\0' * (filesize - output_buffer.tell())) + return cherrypy.lib.static.serve_fileobj(output_buffer, content_type="audio/wav", + name="%s.wav" % duration, filesize=output_buffer.tell()) + + @cherrypy.expose + def nexttrack(self, **kwargs): + '''play silence while spotify connect player is waiting for the next track''' + return self.silence(20) + + @cherrypy.expose + def callback(self, **kwargs): + cherrypy.response.headers['Content-Type'] = 'text/html' + code = kwargs.get("code") + url = "http://localhost:%s/callback?code=%s" % (PROXY_PORT, code) + if cherrypy.request.method.upper() in ['GET', 'POST']: + html = "

Authentication succesfull

" + html += "

You can now close this browser window.

" + html += "" + xbmc.executebuiltin("SetProperty(spotify-token-info,%s,Home)" % url) + log_msg("authkey sent") + return html + + @cherrypy.expose + def playercmd(self, cmd, **kwargs): + if cmd == "start": + cherrypy.response.headers['Content-Type'] = 'text' + log_msg("playback start requested by connect") + xbmc.executebuiltin("RunPlugin(plugin://plugin.audio.spotify/?action=play_connect)") + return "OK" + elif cmd == "stop": + cherrypy.response.headers['Content-Type'] = 'text' + log_msg("playback stop requested by connect") + xbmc.executebuiltin("PlayerControl(Stop)") + return "OK" + +class ProxyRunner(threading.Thread): + __server = None + __root = None + + def __init__(self, spotty): + self.__root = Root(spotty) + log = cherrypy.log + log.screen = True + cherrypy.config.update({ + 'server.socket_host': '127.0.0.1', + 'server.socket_port': PROXY_PORT + }) + self.__server = cherrypy.server.httpserver = CPHTTPServer(cherrypy.server) + threading.Thread.__init__(self) + + def run(self): + conf = { '/': {}} + cherrypy.quickstart(self.__root, '/', conf) + + def get_port(self): + return self.__server.bind_addr[1] + + def get_host(self): + return self.__server.bind_addr[0] + + def stop(self): + cherrypy.engine.exit() + self.join(0) + del self.__root + del self.__server diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..b724113 --- /dev/null +++ b/utils.py @@ -0,0 +1,534 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +''' + plugin.audio.squeezebox + spotty Player for Kodi + utils.py + Various helper methods +''' + +import xbmc +import xbmcvfs +import xbmcgui +import os +import stat +import sys +import urllib +from traceback import format_exc +import requests +import subprocess +import xbmcaddon +import struct +import random +import io +import time +import math +from threading import Thread, Event + + +PROXY_PORT = 52308 +DEBUG = True + +try: + import simplejson as json +except Exception: + import json + +try: + from cStringIO import StringIO +except ImportError: + from io import StringIO + +try: + from cBytesIO import BytesIO +except ImportError: + from io import BytesIO + +ADDON_ID = "plugin.audio.spotify" +KODI_VERSION = int(xbmc.getInfoLabel("System.BuildVersion").split(".")[0]) +KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1) +requests.packages.urllib3.disable_warnings() # disable ssl warnings +SCOPE = [ + "user-read-playback-state", + "user-read-currently-playing", + "user-modify-playback-state", + "playlist-read-private", + "playlist-read-collaborative", + "playlist-modify-public", + "playlist-modify-private", + "user-follow-modify", + "user-follow-read", + "user-library-read", + "user-library-modify", + "user-read-private", + "user-read-email", + "user-read-birthdate", + "user-top-read"] +CLIENTID = '2eb96f9b37494be1824999d58028a305' +CLIENT_SECRET = '038ec3b4555f46eab1169134985b9013' + + +try: + from multiprocessing.pool import ThreadPool + SUPPORTS_POOL = True +except Exception: + SUPPORTS_POOL = False + + +def log_msg(msg, loglevel=xbmc.LOGDEBUG): + '''log message to kodi log''' + if isinstance(msg, str): + msg = msg.encode('utf-8') + if DEBUG: + loglevel = xbmc.LOGNOTICE + xbmc.log("%s --> %s" % (ADDON_ID, msg), level=loglevel) + + +def log_exception(modulename, exceptiondetails): + '''helper to properly log an exception''' + log_msg(format_exc(sys.exc_info()), xbmc.LOGDEBUG) + log_msg("Exception in %s ! --> %s" % (modulename, exceptiondetails), xbmc.LOGWARNING) + + +def addon_setting(settingname, set_value=None): + '''get/set addon setting''' + addon = xbmcaddon.Addon(id=ADDON_ID) + if set_value: + addon.setSetting(settingname, set_value) + else: + return addon.getSetting(settingname) + + +def kill_spotty(): + '''make sure we don't have any (remaining) spotty processes running before we start one''' + if xbmc.getCondVisibility("System.Platform.Windows"): + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + subprocess.Popen(["taskkill", "/IM", "spotty.exe"], startupinfo=startupinfo, shell=True) + else: + os.system("killall spotty") + + +def kill_on_timeout(done, timeout, proc): + if not done.wait(timeout): + proc.kill() + + +def get_token(spotty): + # get authentication token for api - prefer cached version + token_info = None + try: + if spotty.playback_supported: + # try to get a token with spotty + token_info = request_token_spotty(spotty, use_creds=False) + if token_info: + spotty.get_username() # save current username in cached spotty creds + if not token_info: + token_info = request_token_spotty(spotty, use_creds=True) + else: + # request new token with web flow + token_info = request_token_web() + except Exception as exc: + log_exception("utils.get_token", exc) + token_info = None + if not token_info: + log_msg("Couldn't request authentication token. Username/password error ? " + "If you're using a facebook account with Spotify, " + "make sure to generate a device account/password in the Spotify accountdetails.") + return token_info + + +def request_token_spotty(spotty, use_creds=True): + '''request token by using the spotty binary''' + token_info = None + if spotty.playback_supported: + try: + args = ["-t", "--client-id", CLIENTID, "--scope", ",".join(SCOPE), "-n", "temp-spotty"] + done = Event() + spotty = spotty.run_spotty(arguments=args, use_creds=use_creds) + watcher = Thread(target=kill_on_timeout, args=(done, 5, spotty)) + watcher.daemon = True + watcher.start() + stdout, stderr = spotty.communicate() + done.set() + result = None + log_msg("request_token_spotty stdout: %s" % stdout) + for line in stdout.split(): + line = line.strip() + if line.startswith(b"{\"accessToken\""): + result = eval(line) + # transform token info to spotipy compatible format + if result: + token_info = {} + token_info["access_token"] = result["accessToken"] + token_info["expires_in"] = result["expiresIn"] + token_info["token_type"] = result["tokenType"] + token_info["scope"] = ' '.join(result["scope"]) + token_info['expires_at'] = int(time.time()) + token_info['expires_in'] + token_info['refresh_token'] = result["accessToken"] + except Exception as exc: + log_exception(__name__, exc) + return token_info + + +def request_token_web(force=False): + '''request the (initial) auth token by webbrowser''' + import spotipy + from spotipy import oauth2 + xbmcvfs.mkdir("special://profile/addon_data/%s/" % ADDON_ID) + cache_path = "special://profile/addon_data/%s/spotipy.cache" % ADDON_ID + cache_path = xbmc.translatePath(cache_path) + scope = " ".join(SCOPE) + redirect_url = 'http://localhost:%s/callback' % PROXY_PORT + sp_oauth = oauth2.SpotifyOAuth(CLIENTID, CLIENT_SECRET, redirect_url, scope=scope, cache_path=cache_path) + # get token from cache + token_info = sp_oauth.get_cached_token() + if not token_info or force: + # request token by using the webbrowser + p = None + auth_url = sp_oauth.get_authorize_url() + + # show message to user that the browser is going to be launched + dialog = xbmcgui.Dialog() + header = xbmc.getInfoLabel("System.AddonTitle(%s)" % ADDON_ID) + msg = xbmc.getInfoLabel("$ADDON[%s 11049]" % ADDON_ID) + dialog.ok(header, msg) + del dialog + + if xbmc.getCondVisibility("System.Platform.Android"): + # for android we just launch the default android browser + xbmc.executebuiltin("StartAndroidActivity(,android.intent.action.VIEW,,%s)" % auth_url) + else: + # use webbrowser module + import webbrowser + log_msg("Launching system-default browser") + webbrowser.open(auth_url, new=1) + + count = 0 + while not xbmc.getInfoLabel("Window(Home).Property(spotify-token-info)"): + log_msg("Waiting for authentication token...") + xbmc.sleep(2000) + if count == 60: + break + count += 1 + + response = xbmc.getInfoLabel("Window(Home).Property(spotify-token-info)") + xbmc.executebuiltin("ClearProperty(spotify-token-info,Home)") + if response: + response = sp_oauth.parse_response_code(response) + token_info = sp_oauth.get_access_token(response) + xbmc.sleep(2000) # allow enough time for the webbrowser to stop + log_msg("Token from web: %s" % token_info, xbmc.LOGDEBUG) + sp = spotipy.Spotify(token_info['access_token']) + username = sp.me()["id"] + del sp + addon_setting("username", username) + return token_info + + +def create_wave_header(duration): + '''generate a wave header for the stream''' + file = BytesIO() + numsamples = 44100 * duration + channels = 2 + samplerate = 44100 + bitspersample = 16 + + # Generate format chunk + format_chunk_spec = "<4sLHHLLHH" + format_chunk = struct.pack( + format_chunk_spec, + "fmt ".encode(encoding='UTF-8'), # Chunk id + 16, # Size of this chunk (excluding chunk id and this field) + 1, # Audio format, 1 for PCM + channels, # Number of channels + samplerate, # Samplerate, 44100, 48000, etc. + samplerate * channels * (bitspersample // 8), # Byterate + channels * (bitspersample // 8), # Blockalign + bitspersample, # 16 bits for two byte samples, etc. => A METTRE A JOUR - POUR TEST''' + ) + # Generate data chunk + data_chunk_spec = "<4sL" + datasize = numsamples * channels * (bitspersample / 8) + data_chunk = struct.pack( + data_chunk_spec, + "data".encode(encoding='UTF-8'), # Chunk id + int(datasize), # Chunk size (excluding chunk id and this field) + ) + sum_items = [ + #"WAVE" string following size field + 4, + #"fmt " + chunk size field + chunk size + struct.calcsize(format_chunk_spec), + # Size of data chunk spec + data size + struct.calcsize(data_chunk_spec) + datasize + ] + # Generate main header + all_cunks_size = int(sum(sum_items)) + main_header_spec = "<4sL4s" + main_header = struct.pack( + main_header_spec, + "RIFF".encode(encoding='UTF-8'), + all_cunks_size, + "WAVE".encode(encoding='UTF-8') + ) + # Write all the contents in + file.write(main_header) + file.write(format_chunk) + file.write(data_chunk) + + return file.getvalue(), all_cunks_size + 8 + + +def process_method_on_list(method_to_run, items): + '''helper method that processes a method on each listitem with pooling if the system supports it''' + all_items = [] + if SUPPORTS_POOL: + pool = ThreadPool() + try: + all_items = pool.map(method_to_run, items) + except Exception: + # catch exception to prevent threadpool running forever + log_msg(format_exc(sys.exc_info())) + log_msg("Error in %s" % method_to_run) + pool.close() + pool.join() + else: + all_items = [method_to_run(item) for item in items] + all_items = filter(None, all_items) + return all_items + + +def get_track_rating(popularity): + if not popularity: + return 0 + else: + return int(math.ceil(popularity * 6 / 100.0)) - 1 + + +def parse_spotify_track(track, is_album_track=True, silenced=False, is_connect=False): + if "track" in track: + track = track['track'] + if track.get("images"): + thumb = track["images"][0]['url'] + elif track['album'].get("images"): + thumb = track['album']["images"][0]['url'] + else: + thumb = "DefaultMusicSongs" + duration = track['duration_ms'] / 1000 + + if silenced: + url = "http://localhost:%s/silence/%s" % (PROXY_PORT, duration) + else: + url = "http://localhost:%s/track/%s/%s" % (PROXY_PORT, track['id'], duration) + + if is_connect or silenced: + url += "/?connect=true" + + if KODI_VERSION > 17: + li = xbmcgui.ListItem(track['name'], path=url, offscreen=True) + else: + li = xbmcgui.ListItem(track['name'], path=url) + infolabels = { + "title": track['name'], + "genre": " / ".join(track["album"].get("genres", [])), + "year": int(track["album"].get("release_date", "0").split("-")[0]), + "album": track['album']["name"], + "artist": " / ".join([artist["name"] for artist in track["artists"]]), + "rating": str(get_track_rating(track["popularity"])), + "duration": duration + } + if is_album_track: + infolabels["tracknumber"] = track["track_number"] + infolabels["discnumber"] = track["disc_number"] + li.setArt({"thumb": thumb}) + li.setInfo(type="Music", infoLabels=infolabels) + li.setProperty("spotifytrackid", track['id']) + li.setContentLookup(False) + li.setProperty('do_not_analyze', 'true') + li.setMimeType("audio/wave") + return url, li + + +def get_chunks(data, chunksize): + return[data[x:x + chunksize] for x in range(0, len(data), chunksize)] + + +def try_encode(text, encoding="utf-8"): + try: + return text.encode(encoding, "ignore") + except: + return text + + +def try_decode(text, encoding="utf-8"): + try: + return text.decode(encoding, "ignore") + except: + return text + + +def normalize_string(text): + import unicodedata + text = text.replace(":", "") + text = text.replace("/", "-") + text = text.replace("\\", "-") + text = text.replace("<", "") + text = text.replace(">", "") + text = text.replace("*", "") + text = text.replace("?", "") + text = text.replace('|', "") + text = text.replace('(', "") + text = text.replace(')', "") + text = text.replace("\"", "") + text = text.strip() + text = text.rstrip('.') + text = unicodedata.normalize('NFKD', try_decode(text)) + return text + + +def get_playername(): + playername = xbmc.getInfoLabel("System.FriendlyName") + if playername == "Kodi": + import socket + playername = "Kodi - %s" % socket.gethostname() + return playername + + +class Spotty(object): + ''' + spotty is wrapped into a seperate class to store common properties + this is done to prevent hitting a kodi issue where calling one of the infolabel methods + at playback time causes a crash of the playback + ''' + playback_supported = False + playername = None + __spotty_binary = None + __cache_path = None + + def __init__(self): + '''initialize with default values''' + self.__cache_path = xbmc.translatePath("special://profile/addon_data/%s/" % ADDON_ID) + self.playername = get_playername() + self.__spotty_binary = self.get_spotty_binary() + + if self.__spotty_binary and self.test_spotty(self.__spotty_binary): + self.playback_supported = True + xbmc.executebuiltin("SetProperty(spotify.supportsplayback, true, Home)") + else: + log_msg("Error while verifying spotty. Local playback is disabled.") + + def test_spotty(self, binary_path): + '''self-test spotty binary''' + try: + st = os.stat(binary_path) + os.chmod(binary_path, st.st_mode | stat.S_IEXEC) + args = [ + binary_path, + "-n", "selftest", + "-x", "--disable-discovery", + "-v" + ] + startupinfo = None + if os.name == 'nt': + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + spotty = subprocess.Popen( + args, + startupinfo=startupinfo, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + bufsize=0) + stdout, stderr = spotty.communicate() + log_msg(stdout) + if "ok spotty".encode(encoding='UTF-8') in stdout: + return True + elif xbmc.getCondVisibility("System.Platform.Windows"): + log_msg("Unable to initialize spotty binary for playback." + "Make sure you have the VC++ 2015 runtime installed.", xbmc.LOGERROR) + except Exception as exc: + log_exception(__name__, exc) + return False + + def run_spotty(self, arguments=None, use_creds=False, disable_discovery=True, ap_port="54443"): + '''On supported platforms we include spotty binary''' + try: + args = [ + self.__spotty_binary, + "-c", self.__cache_path, + "-b", "320", + "-v", + "--enable-audio-cache", + "--ap-port",ap_port + ] + if use_creds: + # use username/password login for spotty + addon = xbmcaddon.Addon(id=ADDON_ID) + username = addon.getSetting("username") + password = addon.getSetting("password") + del addon + if username and password: + args += ["-u", username, "-p", password] + if disable_discovery: + args += ["--disable-discovery"] + if arguments: + args += arguments + if not "-n" in args: + args += ["-n", self.playername] + startupinfo = None + if os.name == 'nt': + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + return subprocess.Popen(args, startupinfo=startupinfo, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT) + except Exception as exc: + log_exception(__name__, exc) + return None + + def get_spotty_binary(self): + '''find the correct spotty binary belonging to the platform''' + sp_binary = None + if xbmc.getCondVisibility("System.Platform.Windows"): + sp_binary = os.path.join(os.path.dirname(__file__), "spotty", "windows", "spotty.exe") + elif xbmc.getCondVisibility("System.Platform.OSX"): + # macos binary is x86_64 intel + sp_binary = os.path.join(os.path.dirname(__file__), "spotty", "darwin", "spotty") + elif xbmc.getCondVisibility("System.Platform.Linux + !System.Platform.Android"): + # try to find out the correct architecture by trial and error + import platform + architecture = platform.machine() + log_msg("reported architecture: %s" % architecture) + if architecture.startswith('AMD64') or architecture.startswith('x86_64'): + # generic linux x86_64 binary + sp_binary = os.path.join(os.path.dirname(__file__), "spotty", "x86-linux", "spotty-x86_64") + else: + # just try to get the correct binary path if we're unsure about the platform/cpu + paths = [] +## paths.append(os.path.join(os.path.dirname(__file__), "spotty", "arm-linux", "spotty-muslhf")) + paths.append(os.path.join(os.path.dirname(__file__), "spotty", "arm-linux", "spotty-hf")) +## paths.append(os.path.join(os.path.dirname(__file__), "spotty", "arm-linux", "spotty")) + paths.append(os.path.join(os.path.dirname(__file__), "spotty", "x86-linux", "spotty")) + for binary_path in paths: + if self.test_spotty(binary_path): + sp_binary = binary_path + break + if sp_binary: + st = os.stat(sp_binary) + os.chmod(sp_binary, st.st_mode | stat.S_IEXEC) + log_msg("Architecture detected. Using spotty binary %s" % sp_binary) + else: + log_msg("Failed to detect architecture or platform not supported ! Local playback will not be available.") + return sp_binary + + def get_username(self): + ''' obtain/check (last) username of the credentials obtained by spotify connect''' + username = "" + cred_file = xbmc.translatePath("special://profile/addon_data/%s/credentials.json" % ADDON_ID) + if xbmcvfs.exists(cred_file): + with open(cred_file) as cred_file: + data = cred_file.read() + data = eval(data) + username = data["username"] + addon_setting("connect_username", username) + return username From 599323e7926da9ddf82f201e38558186133808d6 Mon Sep 17 00:00:00 2001 From: ldsz <31321633+ldsz@users.noreply.github.com> Date: Wed, 10 Jun 2020 10:09:37 +0200 Subject: [PATCH 03/37] Delete utils.py --- utils.py | 534 ------------------------------------------------------- 1 file changed, 534 deletions(-) delete mode 100644 utils.py diff --git a/utils.py b/utils.py deleted file mode 100644 index b724113..0000000 --- a/utils.py +++ /dev/null @@ -1,534 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- - -''' - plugin.audio.squeezebox - spotty Player for Kodi - utils.py - Various helper methods -''' - -import xbmc -import xbmcvfs -import xbmcgui -import os -import stat -import sys -import urllib -from traceback import format_exc -import requests -import subprocess -import xbmcaddon -import struct -import random -import io -import time -import math -from threading import Thread, Event - - -PROXY_PORT = 52308 -DEBUG = True - -try: - import simplejson as json -except Exception: - import json - -try: - from cStringIO import StringIO -except ImportError: - from io import StringIO - -try: - from cBytesIO import BytesIO -except ImportError: - from io import BytesIO - -ADDON_ID = "plugin.audio.spotify" -KODI_VERSION = int(xbmc.getInfoLabel("System.BuildVersion").split(".")[0]) -KODILANGUAGE = xbmc.getLanguage(xbmc.ISO_639_1) -requests.packages.urllib3.disable_warnings() # disable ssl warnings -SCOPE = [ - "user-read-playback-state", - "user-read-currently-playing", - "user-modify-playback-state", - "playlist-read-private", - "playlist-read-collaborative", - "playlist-modify-public", - "playlist-modify-private", - "user-follow-modify", - "user-follow-read", - "user-library-read", - "user-library-modify", - "user-read-private", - "user-read-email", - "user-read-birthdate", - "user-top-read"] -CLIENTID = '2eb96f9b37494be1824999d58028a305' -CLIENT_SECRET = '038ec3b4555f46eab1169134985b9013' - - -try: - from multiprocessing.pool import ThreadPool - SUPPORTS_POOL = True -except Exception: - SUPPORTS_POOL = False - - -def log_msg(msg, loglevel=xbmc.LOGDEBUG): - '''log message to kodi log''' - if isinstance(msg, str): - msg = msg.encode('utf-8') - if DEBUG: - loglevel = xbmc.LOGNOTICE - xbmc.log("%s --> %s" % (ADDON_ID, msg), level=loglevel) - - -def log_exception(modulename, exceptiondetails): - '''helper to properly log an exception''' - log_msg(format_exc(sys.exc_info()), xbmc.LOGDEBUG) - log_msg("Exception in %s ! --> %s" % (modulename, exceptiondetails), xbmc.LOGWARNING) - - -def addon_setting(settingname, set_value=None): - '''get/set addon setting''' - addon = xbmcaddon.Addon(id=ADDON_ID) - if set_value: - addon.setSetting(settingname, set_value) - else: - return addon.getSetting(settingname) - - -def kill_spotty(): - '''make sure we don't have any (remaining) spotty processes running before we start one''' - if xbmc.getCondVisibility("System.Platform.Windows"): - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - subprocess.Popen(["taskkill", "/IM", "spotty.exe"], startupinfo=startupinfo, shell=True) - else: - os.system("killall spotty") - - -def kill_on_timeout(done, timeout, proc): - if not done.wait(timeout): - proc.kill() - - -def get_token(spotty): - # get authentication token for api - prefer cached version - token_info = None - try: - if spotty.playback_supported: - # try to get a token with spotty - token_info = request_token_spotty(spotty, use_creds=False) - if token_info: - spotty.get_username() # save current username in cached spotty creds - if not token_info: - token_info = request_token_spotty(spotty, use_creds=True) - else: - # request new token with web flow - token_info = request_token_web() - except Exception as exc: - log_exception("utils.get_token", exc) - token_info = None - if not token_info: - log_msg("Couldn't request authentication token. Username/password error ? " - "If you're using a facebook account with Spotify, " - "make sure to generate a device account/password in the Spotify accountdetails.") - return token_info - - -def request_token_spotty(spotty, use_creds=True): - '''request token by using the spotty binary''' - token_info = None - if spotty.playback_supported: - try: - args = ["-t", "--client-id", CLIENTID, "--scope", ",".join(SCOPE), "-n", "temp-spotty"] - done = Event() - spotty = spotty.run_spotty(arguments=args, use_creds=use_creds) - watcher = Thread(target=kill_on_timeout, args=(done, 5, spotty)) - watcher.daemon = True - watcher.start() - stdout, stderr = spotty.communicate() - done.set() - result = None - log_msg("request_token_spotty stdout: %s" % stdout) - for line in stdout.split(): - line = line.strip() - if line.startswith(b"{\"accessToken\""): - result = eval(line) - # transform token info to spotipy compatible format - if result: - token_info = {} - token_info["access_token"] = result["accessToken"] - token_info["expires_in"] = result["expiresIn"] - token_info["token_type"] = result["tokenType"] - token_info["scope"] = ' '.join(result["scope"]) - token_info['expires_at'] = int(time.time()) + token_info['expires_in'] - token_info['refresh_token'] = result["accessToken"] - except Exception as exc: - log_exception(__name__, exc) - return token_info - - -def request_token_web(force=False): - '''request the (initial) auth token by webbrowser''' - import spotipy - from spotipy import oauth2 - xbmcvfs.mkdir("special://profile/addon_data/%s/" % ADDON_ID) - cache_path = "special://profile/addon_data/%s/spotipy.cache" % ADDON_ID - cache_path = xbmc.translatePath(cache_path) - scope = " ".join(SCOPE) - redirect_url = 'http://localhost:%s/callback' % PROXY_PORT - sp_oauth = oauth2.SpotifyOAuth(CLIENTID, CLIENT_SECRET, redirect_url, scope=scope, cache_path=cache_path) - # get token from cache - token_info = sp_oauth.get_cached_token() - if not token_info or force: - # request token by using the webbrowser - p = None - auth_url = sp_oauth.get_authorize_url() - - # show message to user that the browser is going to be launched - dialog = xbmcgui.Dialog() - header = xbmc.getInfoLabel("System.AddonTitle(%s)" % ADDON_ID) - msg = xbmc.getInfoLabel("$ADDON[%s 11049]" % ADDON_ID) - dialog.ok(header, msg) - del dialog - - if xbmc.getCondVisibility("System.Platform.Android"): - # for android we just launch the default android browser - xbmc.executebuiltin("StartAndroidActivity(,android.intent.action.VIEW,,%s)" % auth_url) - else: - # use webbrowser module - import webbrowser - log_msg("Launching system-default browser") - webbrowser.open(auth_url, new=1) - - count = 0 - while not xbmc.getInfoLabel("Window(Home).Property(spotify-token-info)"): - log_msg("Waiting for authentication token...") - xbmc.sleep(2000) - if count == 60: - break - count += 1 - - response = xbmc.getInfoLabel("Window(Home).Property(spotify-token-info)") - xbmc.executebuiltin("ClearProperty(spotify-token-info,Home)") - if response: - response = sp_oauth.parse_response_code(response) - token_info = sp_oauth.get_access_token(response) - xbmc.sleep(2000) # allow enough time for the webbrowser to stop - log_msg("Token from web: %s" % token_info, xbmc.LOGDEBUG) - sp = spotipy.Spotify(token_info['access_token']) - username = sp.me()["id"] - del sp - addon_setting("username", username) - return token_info - - -def create_wave_header(duration): - '''generate a wave header for the stream''' - file = BytesIO() - numsamples = 44100 * duration - channels = 2 - samplerate = 44100 - bitspersample = 16 - - # Generate format chunk - format_chunk_spec = "<4sLHHLLHH" - format_chunk = struct.pack( - format_chunk_spec, - "fmt ".encode(encoding='UTF-8'), # Chunk id - 16, # Size of this chunk (excluding chunk id and this field) - 1, # Audio format, 1 for PCM - channels, # Number of channels - samplerate, # Samplerate, 44100, 48000, etc. - samplerate * channels * (bitspersample // 8), # Byterate - channels * (bitspersample // 8), # Blockalign - bitspersample, # 16 bits for two byte samples, etc. => A METTRE A JOUR - POUR TEST''' - ) - # Generate data chunk - data_chunk_spec = "<4sL" - datasize = numsamples * channels * (bitspersample / 8) - data_chunk = struct.pack( - data_chunk_spec, - "data".encode(encoding='UTF-8'), # Chunk id - int(datasize), # Chunk size (excluding chunk id and this field) - ) - sum_items = [ - #"WAVE" string following size field - 4, - #"fmt " + chunk size field + chunk size - struct.calcsize(format_chunk_spec), - # Size of data chunk spec + data size - struct.calcsize(data_chunk_spec) + datasize - ] - # Generate main header - all_cunks_size = int(sum(sum_items)) - main_header_spec = "<4sL4s" - main_header = struct.pack( - main_header_spec, - "RIFF".encode(encoding='UTF-8'), - all_cunks_size, - "WAVE".encode(encoding='UTF-8') - ) - # Write all the contents in - file.write(main_header) - file.write(format_chunk) - file.write(data_chunk) - - return file.getvalue(), all_cunks_size + 8 - - -def process_method_on_list(method_to_run, items): - '''helper method that processes a method on each listitem with pooling if the system supports it''' - all_items = [] - if SUPPORTS_POOL: - pool = ThreadPool() - try: - all_items = pool.map(method_to_run, items) - except Exception: - # catch exception to prevent threadpool running forever - log_msg(format_exc(sys.exc_info())) - log_msg("Error in %s" % method_to_run) - pool.close() - pool.join() - else: - all_items = [method_to_run(item) for item in items] - all_items = filter(None, all_items) - return all_items - - -def get_track_rating(popularity): - if not popularity: - return 0 - else: - return int(math.ceil(popularity * 6 / 100.0)) - 1 - - -def parse_spotify_track(track, is_album_track=True, silenced=False, is_connect=False): - if "track" in track: - track = track['track'] - if track.get("images"): - thumb = track["images"][0]['url'] - elif track['album'].get("images"): - thumb = track['album']["images"][0]['url'] - else: - thumb = "DefaultMusicSongs" - duration = track['duration_ms'] / 1000 - - if silenced: - url = "http://localhost:%s/silence/%s" % (PROXY_PORT, duration) - else: - url = "http://localhost:%s/track/%s/%s" % (PROXY_PORT, track['id'], duration) - - if is_connect or silenced: - url += "/?connect=true" - - if KODI_VERSION > 17: - li = xbmcgui.ListItem(track['name'], path=url, offscreen=True) - else: - li = xbmcgui.ListItem(track['name'], path=url) - infolabels = { - "title": track['name'], - "genre": " / ".join(track["album"].get("genres", [])), - "year": int(track["album"].get("release_date", "0").split("-")[0]), - "album": track['album']["name"], - "artist": " / ".join([artist["name"] for artist in track["artists"]]), - "rating": str(get_track_rating(track["popularity"])), - "duration": duration - } - if is_album_track: - infolabels["tracknumber"] = track["track_number"] - infolabels["discnumber"] = track["disc_number"] - li.setArt({"thumb": thumb}) - li.setInfo(type="Music", infoLabels=infolabels) - li.setProperty("spotifytrackid", track['id']) - li.setContentLookup(False) - li.setProperty('do_not_analyze', 'true') - li.setMimeType("audio/wave") - return url, li - - -def get_chunks(data, chunksize): - return[data[x:x + chunksize] for x in range(0, len(data), chunksize)] - - -def try_encode(text, encoding="utf-8"): - try: - return text.encode(encoding, "ignore") - except: - return text - - -def try_decode(text, encoding="utf-8"): - try: - return text.decode(encoding, "ignore") - except: - return text - - -def normalize_string(text): - import unicodedata - text = text.replace(":", "") - text = text.replace("/", "-") - text = text.replace("\\", "-") - text = text.replace("<", "") - text = text.replace(">", "") - text = text.replace("*", "") - text = text.replace("?", "") - text = text.replace('|', "") - text = text.replace('(', "") - text = text.replace(')', "") - text = text.replace("\"", "") - text = text.strip() - text = text.rstrip('.') - text = unicodedata.normalize('NFKD', try_decode(text)) - return text - - -def get_playername(): - playername = xbmc.getInfoLabel("System.FriendlyName") - if playername == "Kodi": - import socket - playername = "Kodi - %s" % socket.gethostname() - return playername - - -class Spotty(object): - ''' - spotty is wrapped into a seperate class to store common properties - this is done to prevent hitting a kodi issue where calling one of the infolabel methods - at playback time causes a crash of the playback - ''' - playback_supported = False - playername = None - __spotty_binary = None - __cache_path = None - - def __init__(self): - '''initialize with default values''' - self.__cache_path = xbmc.translatePath("special://profile/addon_data/%s/" % ADDON_ID) - self.playername = get_playername() - self.__spotty_binary = self.get_spotty_binary() - - if self.__spotty_binary and self.test_spotty(self.__spotty_binary): - self.playback_supported = True - xbmc.executebuiltin("SetProperty(spotify.supportsplayback, true, Home)") - else: - log_msg("Error while verifying spotty. Local playback is disabled.") - - def test_spotty(self, binary_path): - '''self-test spotty binary''' - try: - st = os.stat(binary_path) - os.chmod(binary_path, st.st_mode | stat.S_IEXEC) - args = [ - binary_path, - "-n", "selftest", - "-x", "--disable-discovery", - "-v" - ] - startupinfo = None - if os.name == 'nt': - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - spotty = subprocess.Popen( - args, - startupinfo=startupinfo, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - bufsize=0) - stdout, stderr = spotty.communicate() - log_msg(stdout) - if "ok spotty".encode(encoding='UTF-8') in stdout: - return True - elif xbmc.getCondVisibility("System.Platform.Windows"): - log_msg("Unable to initialize spotty binary for playback." - "Make sure you have the VC++ 2015 runtime installed.", xbmc.LOGERROR) - except Exception as exc: - log_exception(__name__, exc) - return False - - def run_spotty(self, arguments=None, use_creds=False, disable_discovery=True, ap_port="54443"): - '''On supported platforms we include spotty binary''' - try: - args = [ - self.__spotty_binary, - "-c", self.__cache_path, - "-b", "320", - "-v", - "--enable-audio-cache", - "--ap-port",ap_port - ] - if use_creds: - # use username/password login for spotty - addon = xbmcaddon.Addon(id=ADDON_ID) - username = addon.getSetting("username") - password = addon.getSetting("password") - del addon - if username and password: - args += ["-u", username, "-p", password] - if disable_discovery: - args += ["--disable-discovery"] - if arguments: - args += arguments - if not "-n" in args: - args += ["-n", self.playername] - startupinfo = None - if os.name == 'nt': - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - return subprocess.Popen(args, startupinfo=startupinfo, stdout=subprocess.PIPE, - stderr=subprocess.STDOUT) - except Exception as exc: - log_exception(__name__, exc) - return None - - def get_spotty_binary(self): - '''find the correct spotty binary belonging to the platform''' - sp_binary = None - if xbmc.getCondVisibility("System.Platform.Windows"): - sp_binary = os.path.join(os.path.dirname(__file__), "spotty", "windows", "spotty.exe") - elif xbmc.getCondVisibility("System.Platform.OSX"): - # macos binary is x86_64 intel - sp_binary = os.path.join(os.path.dirname(__file__), "spotty", "darwin", "spotty") - elif xbmc.getCondVisibility("System.Platform.Linux + !System.Platform.Android"): - # try to find out the correct architecture by trial and error - import platform - architecture = platform.machine() - log_msg("reported architecture: %s" % architecture) - if architecture.startswith('AMD64') or architecture.startswith('x86_64'): - # generic linux x86_64 binary - sp_binary = os.path.join(os.path.dirname(__file__), "spotty", "x86-linux", "spotty-x86_64") - else: - # just try to get the correct binary path if we're unsure about the platform/cpu - paths = [] -## paths.append(os.path.join(os.path.dirname(__file__), "spotty", "arm-linux", "spotty-muslhf")) - paths.append(os.path.join(os.path.dirname(__file__), "spotty", "arm-linux", "spotty-hf")) -## paths.append(os.path.join(os.path.dirname(__file__), "spotty", "arm-linux", "spotty")) - paths.append(os.path.join(os.path.dirname(__file__), "spotty", "x86-linux", "spotty")) - for binary_path in paths: - if self.test_spotty(binary_path): - sp_binary = binary_path - break - if sp_binary: - st = os.stat(sp_binary) - os.chmod(sp_binary, st.st_mode | stat.S_IEXEC) - log_msg("Architecture detected. Using spotty binary %s" % sp_binary) - else: - log_msg("Failed to detect architecture or platform not supported ! Local playback will not be available.") - return sp_binary - - def get_username(self): - ''' obtain/check (last) username of the credentials obtained by spotify connect''' - username = "" - cred_file = xbmc.translatePath("special://profile/addon_data/%s/credentials.json" % ADDON_ID) - if xbmcvfs.exists(cred_file): - with open(cred_file) as cred_file: - data = cred_file.read() - data = eval(data) - username = data["username"] - addon_setting("connect_username", username) - return username From d8023dd79ddff4a718487f53ab3350dd75ab2787 Mon Sep 17 00:00:00 2001 From: ldsz <31321633+ldsz@users.noreply.github.com> Date: Wed, 10 Jun 2020 10:09:51 +0200 Subject: [PATCH 04/37] Delete httpproxy.py --- httpproxy.py | 257 --------------------------------------------------- 1 file changed, 257 deletions(-) delete mode 100644 httpproxy.py diff --git a/httpproxy.py b/httpproxy.py deleted file mode 100644 index 76dfab4..0000000 --- a/httpproxy.py +++ /dev/null @@ -1,257 +0,0 @@ -# -*- coding: utf-8 -*- -import threading -import _thread -import time -import re -import struct -import cherrypy -from cherrypy._cpnative_server import CPHTTPServer -from datetime import datetime -import random -import sys -import platform -import logging -import os -from utils import log_msg, log_exception, create_wave_header, PROXY_PORT, StringIO -import xbmc -import math - -class Root: - spotty = None - - spotty_bin = None - spotty_trackid = None - spotty_range_l = None - - def __init__(self, spotty): - self.__spotty = spotty - - def _check_request(self): - method = cherrypy.request.method.upper() - headers = cherrypy.request.headers - # Fail for other methods than get or head - if method not in ("GET", "HEAD"): - raise cherrypy.HTTPError(405) - # Error if the requester is not allowed - # for now this is a simple check just checking if the useragent matches Kodi - user_agent = headers['User-Agent'].lower() - # if not ("Kodi" in user_agent or "osmc" in user_agent): - # raise cherrypy.HTTPError(403) - return method - - - @cherrypy.expose - def index(self): - return "Server started" - @cherrypy.tools.json_out() - @cherrypy.tools.json_in() - def lms(self, filename, **kwargs): - ''' fake lms hook to retrieve events form spotty daemon''' - method = cherrypy.request.method.upper() - if method != "POST" or filename != "jsonrpc.js": - raise cherrypy.HTTPError(405) - input_json = cherrypy.request.json - if input_json and input_json.get("params"): - event = input_json["params"][1] - log_msg("lms event hook called. Event: %s" % event) - # check username, it might have changed - spotty_user = self.__spotty.get_username() - cur_user = xbmc.getInfoLabel("Window(Home).Property(spotify-username)").decode("utf-8") - if spotty_user != cur_user: - log_msg("user change detected") - xbmc.executebuiltin("SetProperty(spotify-cmd,__LOGOUT__,Home)") - if "start" in event: - log_msg("playback start requested by connect") - xbmc.executebuiltin("RunPlugin(plugin://plugin.audio.spotify/?action=play_connect)") - elif "change" in event: - log_msg("playback change requested by connect") - # we ignore this as track changes are - #xbmc.executebuiltin("RunPlugin(plugin://plugin.audio.spotify/?action=play_connect)") - elif "stop" in event: - log_msg("playback stop requested by connect") - xbmc.executebuiltin("PlayerControl(Stop)") - elif "volume" in event: - vol_level = event[2] - log_msg("volume change detected on connect player: %s" % vol_level) - # ignore for now as it needs more work - #xbmc.executebuiltin("SetVolume(%s,true)" % vol_level) - return {"operation": "request", "result": "success"} - - @cherrypy.expose - def track(self, track_id, duration, **kwargs): - # Check sanity of the request - self._check_request() - - # Calculate file size, and obtain the header - duration = int(float(duration)) - wave_header, filesize = create_wave_header(duration) - request_range = cherrypy.request.headers.get('Range', '') - # response timeout must be at least the duration of the track: read/write loop - # checks for timeout and stops pushing audio to player if it occurs - cherrypy.response.timeout = int(math.ceil(duration * 1.5)) - - range_l = 0 - range_r = filesize - - # headers - if request_range and request_range != "bytes=0-": - # partial request - cherrypy.response.status = '206 Partial Content' - cherrypy.response.headers['Content-Type'] = 'audio/x-wav' - range = cherrypy.request.headers["Range"].split("bytes=")[1].split("-") - log_msg("request header range: %s" % (cherrypy.request.headers['Range']), xbmc.LOGDEBUG) - range_l = int(range[0]) - try: - range_r = int(range[1]) - except: - range_r = filesize - - cherrypy.response.headers['Accept-Ranges'] = 'bytes' - cherrypy.response.headers['Content-Length'] = filesize - cherrypy.response.headers['Content-Range'] = "bytes %s-%s/%s" % (range_l, range_r, filesize) - log_msg("partial request range: %s, length: %s" % (cherrypy.response.headers['Content-Range'], cherrypy.response.headers['Content-Length']), xbmc.LOGDEBUG) - else: - # full file - cherrypy.response.headers['Content-Type'] = 'audio/x-wav' - cherrypy.response.headers['Accept-Ranges'] = 'bytes' - cherrypy.response.headers['Content-Length'] = filesize - log_msg("!! Full File. Size : %s " % (filesize), xbmc.LOGDEBUG) - - # If method was GET, write the file content - if cherrypy.request.method.upper() == 'GET': - return self.send_audio_stream(track_id, filesize, wave_header, range_l) - track._cp_config = {'response.stream': True} - - def kill_spotty(self): - self.spotty_bin.terminate() - self.spotty_bin = None - self.spotty_trackid = None - self.spotty_range_l = None - - def send_audio_stream(self, track_id, filesize, wave_header, range_l): - '''chunked transfer of audio data from spotty binary''' - if self.spotty_bin != None and \ - self.spotty_trackid == track_id and \ - self.spotty_range_l == range_l: - # leave the existing spotty running and don't start a new one. - log_msg("WHOOPS!!! Running spotty still handling same request - leave it alone.", \ - xbmc.LOGERROR) - return - elif self.spotty_bin != None: - # If spotty binary still attached for a different request, try to terminate it. - log_msg("WHOOPS!!! Running spotty detected - killing it to continue.", \ - xbmc.LOGERROR) - self.kill_spotty() - - log_msg("start transfer for track %s - range: %s" % (track_id, range_l), \ - xbmc.LOGDEBUG) - try: - # Initialize some loop vars - max_buffer_size = 524288 - bytes_written = 0 - - # Write wave header - # only count bytes actually from the spotify stream - # bytes_written = len(wave_header) - if not range_l: - yield wave_header - - # get OGG data from spotty stdout and append to our buffer - args = ["-n", "temp", "--single-track", track_id] - self.spotty_bin = self.__spotty.run_spotty(args, use_creds=True) - self.spotty_trackid = track_id - self.spotty_range_l = range_l - log_msg("Infos : Track : %s" % track_id) - - - # ignore the first x bytes to match the range request - if range_l: - self.spotty_bin.stdout.read(range_l) - - # Loop as long as there's something to output - frame = self.spotty_bin.stdout.read(max_buffer_size) - while frame: - bytes_written += len(frame) - yield frame - frame = self.spotty_bin.stdout.read(max_buffer_size) - except Exception as exc: - log_exception(__name__, exc) - finally: - # make sure spotty always gets terminated - if self.spotty_bin != None: - self.kill_spotty() - log_msg("FINISH transfer for track %s - range %s - written %s" % (track_id, range_l, bytes_written), \ - xbmc.LOGDEBUG) - - @cherrypy.expose - def silence(self, duration, **kwargs): - '''stream silence audio for the given duration, used by spotify connect player''' - duration = int(duration) - wave_header, filesize = create_wave_header(duration) - output_buffer = StringIO() - output_buffer.write(wave_header) - output_buffer.write('\0' * (filesize - output_buffer.tell())) - return cherrypy.lib.static.serve_fileobj(output_buffer, content_type="audio/wav", - name="%s.wav" % duration, filesize=output_buffer.tell()) - - @cherrypy.expose - def nexttrack(self, **kwargs): - '''play silence while spotify connect player is waiting for the next track''' - return self.silence(20) - - @cherrypy.expose - def callback(self, **kwargs): - cherrypy.response.headers['Content-Type'] = 'text/html' - code = kwargs.get("code") - url = "http://localhost:%s/callback?code=%s" % (PROXY_PORT, code) - if cherrypy.request.method.upper() in ['GET', 'POST']: - html = "

Authentication succesfull

" - html += "

You can now close this browser window.

" - html += "" - xbmc.executebuiltin("SetProperty(spotify-token-info,%s,Home)" % url) - log_msg("authkey sent") - return html - - @cherrypy.expose - def playercmd(self, cmd, **kwargs): - if cmd == "start": - cherrypy.response.headers['Content-Type'] = 'text' - log_msg("playback start requested by connect") - xbmc.executebuiltin("RunPlugin(plugin://plugin.audio.spotify/?action=play_connect)") - return "OK" - elif cmd == "stop": - cherrypy.response.headers['Content-Type'] = 'text' - log_msg("playback stop requested by connect") - xbmc.executebuiltin("PlayerControl(Stop)") - return "OK" - -class ProxyRunner(threading.Thread): - __server = None - __root = None - - def __init__(self, spotty): - self.__root = Root(spotty) - log = cherrypy.log - log.screen = True - cherrypy.config.update({ - 'server.socket_host': '127.0.0.1', - 'server.socket_port': PROXY_PORT - }) - self.__server = cherrypy.server.httpserver = CPHTTPServer(cherrypy.server) - threading.Thread.__init__(self) - - def run(self): - conf = { '/': {}} - cherrypy.quickstart(self.__root, '/', conf) - - def get_port(self): - return self.__server.bind_addr[1] - - def get_host(self): - return self.__server.bind_addr[0] - - def stop(self): - cherrypy.engine.exit() - self.join(0) - del self.__root - del self.__server From d7d8a57f1f3c84930fffe571bfb229a993a2987a Mon Sep 17 00:00:00 2001 From: ldsz <31321633+ldsz@users.noreply.github.com> Date: Wed, 10 Jun 2020 10:10:13 +0200 Subject: [PATCH 05/37] Add files via upload --- resources/lib/httpproxy.py | 38 ++++++++++++++++--------------------- resources/lib/utils.py | 39 ++++++++++++++++++++++---------------- 2 files changed, 39 insertions(+), 38 deletions(-) diff --git a/resources/lib/httpproxy.py b/resources/lib/httpproxy.py index d8d8070..76dfab4 100644 --- a/resources/lib/httpproxy.py +++ b/resources/lib/httpproxy.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- import threading -import thread +import _thread import time import re import struct @@ -35,12 +35,14 @@ def _check_request(self): # Error if the requester is not allowed # for now this is a simple check just checking if the useragent matches Kodi user_agent = headers['User-Agent'].lower() - if not ("kodi" in user_agent or "osmc" in user_agent): - raise cherrypy.HTTPError(403) + # if not ("Kodi" in user_agent or "osmc" in user_agent): + # raise cherrypy.HTTPError(403) return method @cherrypy.expose + def index(self): + return "Server started" @cherrypy.tools.json_out() @cherrypy.tools.json_in() def lms(self, filename, **kwargs): @@ -81,7 +83,7 @@ def track(self, track_id, duration, **kwargs): self._check_request() # Calculate file size, and obtain the header - duration = int(duration) + duration = int(float(duration)) wave_header, filesize = create_wave_header(duration) request_range = cherrypy.request.headers.get('Range', '') # response timeout must be at least the duration of the track: read/write loop @@ -113,6 +115,7 @@ def track(self, track_id, duration, **kwargs): cherrypy.response.headers['Content-Type'] = 'audio/x-wav' cherrypy.response.headers['Accept-Ranges'] = 'bytes' cherrypy.response.headers['Content-Length'] = filesize + log_msg("!! Full File. Size : %s " % (filesize), xbmc.LOGDEBUG) # If method was GET, write the file content if cherrypy.request.method.upper() == 'GET': @@ -153,26 +156,21 @@ def send_audio_stream(self, track_id, filesize, wave_header, range_l): if not range_l: yield wave_header - # get pcm data from spotty stdout and append to our buffer + # get OGG data from spotty stdout and append to our buffer args = ["-n", "temp", "--single-track", track_id] self.spotty_bin = self.__spotty.run_spotty(args, use_creds=True) self.spotty_trackid = track_id self.spotty_range_l = range_l - - # ignore the first x bytes to match the range request + log_msg("Infos : Track : %s" % track_id) + + + # ignore the first x bytes to match the range request if range_l: self.spotty_bin.stdout.read(range_l) # Loop as long as there's something to output frame = self.spotty_bin.stdout.read(max_buffer_size) while frame: - if cherrypy.response.timed_out: - # A timeout occured on the cherrypy session and has been flagged - so exit - # The session timer was set to be longer than the track being played so this - # would probably require network problems or something bad elsewhere. - log_msg("SPOTTY cherrypy response timeout: %r - %s" % \ - (repr(cherrypy.response.timed_out), cherrypy.response.status), xbmc.LOGERROR) - break bytes_written += len(frame) yield frame frame = self.spotty_bin.stdout.read(max_buffer_size) @@ -182,7 +180,7 @@ def send_audio_stream(self, track_id, filesize, wave_header, range_l): # make sure spotty always gets terminated if self.spotty_bin != None: self.kill_spotty() - log_msg("FINISH transfer for track %s - range %s" % (track_id, range_l), \ + log_msg("FINISH transfer for track %s - range %s - written %s" % (track_id, range_l, bytes_written), \ xbmc.LOGDEBUG) @cherrypy.expose @@ -234,14 +232,10 @@ class ProxyRunner(threading.Thread): def __init__(self, spotty): self.__root = Root(spotty) log = cherrypy.log - log.access_file = '' - log.error_file = '' - log.screen = False + log.screen = True cherrypy.config.update({ - 'server.socket_host': '0.0.0.0', - 'server.socket_port': PROXY_PORT, - 'engine.timeout_monitor.frequency': 5, - 'server.shutdown_timeout': 1 + 'server.socket_host': '127.0.0.1', + 'server.socket_port': PROXY_PORT }) self.__server = cherrypy.server.httpserver = CPHTTPServer(cherrypy.server) threading.Thread.__init__(self) diff --git a/resources/lib/utils.py b/resources/lib/utils.py index b2e7df9..b724113 100644 --- a/resources/lib/utils.py +++ b/resources/lib/utils.py @@ -40,6 +40,10 @@ except ImportError: from io import StringIO +try: + from cBytesIO import BytesIO +except ImportError: + from io import BytesIO ADDON_ID = "plugin.audio.spotify" KODI_VERSION = int(xbmc.getInfoLabel("System.BuildVersion").split(".")[0]) @@ -225,7 +229,7 @@ def request_token_web(force=False): def create_wave_header(duration): '''generate a wave header for the stream''' - file = StringIO() + file = BytesIO() numsamples = 44100 * duration channels = 2 samplerate = 44100 @@ -235,21 +239,21 @@ def create_wave_header(duration): format_chunk_spec = "<4sLHHLLHH" format_chunk = struct.pack( format_chunk_spec, - "fmt ", # Chunk id + "fmt ".encode(encoding='UTF-8'), # Chunk id 16, # Size of this chunk (excluding chunk id and this field) 1, # Audio format, 1 for PCM channels, # Number of channels samplerate, # Samplerate, 44100, 48000, etc. - samplerate * channels * (bitspersample / 8), # Byterate - channels * (bitspersample / 8), # Blockalign - bitspersample, # 16 bits for two byte samples, etc. + samplerate * channels * (bitspersample // 8), # Byterate + channels * (bitspersample // 8), # Blockalign + bitspersample, # 16 bits for two byte samples, etc. => A METTRE A JOUR - POUR TEST''' ) # Generate data chunk data_chunk_spec = "<4sL" datasize = numsamples * channels * (bitspersample / 8) data_chunk = struct.pack( data_chunk_spec, - "data", # Chunk id + "data".encode(encoding='UTF-8'), # Chunk id int(datasize), # Chunk size (excluding chunk id and this field) ) sum_items = [ @@ -265,9 +269,9 @@ def create_wave_header(duration): main_header_spec = "<4sL4s" main_header = struct.pack( main_header_spec, - "RIFF", + "RIFF".encode(encoding='UTF-8'), all_cunks_size, - "WAVE" + "WAVE".encode(encoding='UTF-8') ) # Write all the contents in file.write(main_header) @@ -423,7 +427,8 @@ def test_spotty(self, binary_path): args = [ binary_path, "-n", "selftest", - "-x", "--disable-discovery" + "-x", "--disable-discovery", + "-v" ] startupinfo = None if os.name == 'nt': @@ -437,23 +442,25 @@ def test_spotty(self, binary_path): bufsize=0) stdout, stderr = spotty.communicate() log_msg(stdout) - '''if "ok spotty" in stdout:''' - return True - '''elif xbmc.getCondVisibility("System.Platform.Windows"): + if "ok spotty".encode(encoding='UTF-8') in stdout: + return True + elif xbmc.getCondVisibility("System.Platform.Windows"): log_msg("Unable to initialize spotty binary for playback." - "Make sure you have the VC++ 2015 runtime installed.", xbmc.LOGERROR)''' + "Make sure you have the VC++ 2015 runtime installed.", xbmc.LOGERROR) except Exception as exc: log_exception(__name__, exc) return False - def run_spotty(self, arguments=None, use_creds=False, disable_discovery=True, ap_port="443"): + def run_spotty(self, arguments=None, use_creds=False, disable_discovery=True, ap_port="54443"): '''On supported platforms we include spotty binary''' try: args = [ self.__spotty_binary, "-c", self.__cache_path, -## "-b", "320" - "--ap-port",ap_port + "-b", "320", + "-v", + "--enable-audio-cache", + "--ap-port",ap_port ] if use_creds: # use username/password login for spotty From 69a9f84be9d00ded55fe0e79efe3c45678261881 Mon Sep 17 00:00:00 2001 From: ldsz <31321633+ldsz@users.noreply.github.com> Date: Wed, 10 Jun 2020 10:10:59 +0200 Subject: [PATCH 06/37] Add files via upload --- addon.xml | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/addon.xml b/addon.xml index 83f2517..48b0a78 100644 --- a/addon.xml +++ b/addon.xml @@ -1,13 +1,12 @@ - + - - - - - + + + + audio From 5ef5be2da5e70654e55aea0fc1683e58712034c9 Mon Sep 17 00:00:00 2001 From: ldsz <31321633+ldsz@users.noreply.github.com> Date: Wed, 10 Jun 2020 10:24:05 +0200 Subject: [PATCH 07/37] Modded for Kodi 19 Modification to make it work with Kodi 19 --- resources/lib/backports/__init__.py | 1 + .../lib/backports/functools_lru_cache.py | 184 + resources/lib/cheroot/__init__.py | 6 + resources/lib/cheroot/__main__.py | 6 + .../__pycache__/__init__.cpython-37.opt-1.pyc | Bin 0 -> 405 bytes .../__pycache__/_compat.cpython-37.opt-1.pyc | Bin 0 -> 2104 bytes .../__pycache__/errors.cpython-37.opt-1.pyc | Bin 0 -> 2409 bytes .../__pycache__/makefile.cpython-37.opt-1.pyc | Bin 0 -> 7077 bytes .../__pycache__/server.cpython-37.opt-1.pyc | Bin 0 -> 45642 bytes resources/lib/cheroot/_compat.py | 66 + resources/lib/cheroot/cli.py | 233 ++ resources/lib/cheroot/errors.py | 58 + resources/lib/cheroot/makefile.py | 387 ++ resources/lib/cheroot/server.py | 2001 +++++++++++ resources/lib/cheroot/ssl/__init__.py | 51 + resources/lib/cheroot/ssl/builtin.py | 162 + resources/lib/cheroot/ssl/pyopenssl.py | 267 ++ resources/lib/cheroot/test/__init__.py | 1 + resources/lib/cheroot/test/conftest.py | 27 + resources/lib/cheroot/test/helper.py | 169 + resources/lib/cheroot/test/test.pem | 38 + resources/lib/cheroot/test/test__compat.py | 49 + resources/lib/cheroot/test/test_conn.py | 897 +++++ resources/lib/cheroot/test/test_core.py | 405 +++ resources/lib/cheroot/test/test_server.py | 193 + resources/lib/cheroot/test/webtest.py | 581 +++ resources/lib/cheroot/testing.py | 144 + resources/lib/cheroot/workers/__init__.py | 1 + .../__pycache__/__init__.cpython-37.opt-1.pyc | Bin 0 -> 220 bytes .../threadpool.cpython-37.opt-1.pyc | Bin 0 -> 8031 bytes resources/lib/cheroot/workers/threadpool.py | 271 ++ resources/lib/cheroot/wsgi.py | 423 +++ resources/lib/cherrypy/__init__.py | 370 ++ resources/lib/cherrypy/__main__.py | 5 + .../__pycache__/__init__.cpython-37.opt-1.pyc | Bin 0 -> 11421 bytes .../_cpchecker.cpython-37.opt-1.pyc | Bin 0 -> 10406 bytes .../_cpcompat.cpython-37.opt-1.pyc | Bin 0 -> 4601 bytes .../_cpconfig.cpython-37.opt-1.pyc | Bin 0 -> 8919 bytes .../_cpdispatch.cpython-37.opt-1.pyc | Bin 0 -> 18276 bytes .../__pycache__/_cperror.cpython-37.opt-1.pyc | Bin 0 -> 18080 bytes .../_cplogging.cpython-37.opt-1.pyc | Bin 0 -> 15137 bytes .../_cpnative_server.cpython-37.opt-1.pyc | Bin 0 -> 4335 bytes .../_cpreqbody.cpython-37.opt-1.pyc | Bin 0 -> 23141 bytes .../_cprequest.cpython-37.opt-1.pyc | Bin 0 -> 16272 bytes .../_cpserver.cpython-37.opt-1.pyc | Bin 0 -> 4412 bytes .../__pycache__/_cptools.cpython-37.opt-1.pyc | Bin 0 -> 17541 bytes .../__pycache__/_cptree.cpython-37.opt-1.pyc | Bin 0 -> 9286 bytes .../__pycache__/_cpwsgi.cpython-37.opt-1.pyc | Bin 0 -> 11479 bytes .../__pycache__/_helper.cpython-37.opt-1.pyc | Bin 0 -> 8755 bytes resources/lib/cherrypy/_cpchecker.py | 325 ++ resources/lib/cherrypy/_cpcompat.py | 162 + resources/lib/cherrypy/_cpconfig.py | 296 ++ resources/lib/cherrypy/_cpdispatch.py | 686 ++++ resources/lib/cherrypy/_cperror.py | 619 ++++ resources/lib/cherrypy/_cplogging.py | 482 +++ resources/lib/cherrypy/_cpmodpy.py | 356 ++ resources/lib/cherrypy/_cpnative_server.py | 168 + resources/lib/cherrypy/_cpreqbody.py | 1000 ++++++ resources/lib/cherrypy/_cprequest.py | 930 +++++ resources/lib/cherrypy/_cpserver.py | 252 ++ resources/lib/cherrypy/_cptools.py | 509 +++ resources/lib/cherrypy/_cptree.py | 313 ++ resources/lib/cherrypy/_cpwsgi.py | 467 +++ resources/lib/cherrypy/_cpwsgi_server.py | 110 + resources/lib/cherrypy/_helper.py | 344 ++ resources/lib/cherrypy/daemon.py | 107 + resources/lib/cherrypy/favicon.ico | Bin 0 -> 1406 bytes resources/lib/cherrypy/lib/__init__.py | 96 + .../__pycache__/__init__.cpython-37.opt-1.pyc | Bin 0 -> 2789 bytes .../auth_basic.cpython-37.opt-1.pyc | Bin 0 -> 3929 bytes .../auth_digest.cpython-37.opt-1.pyc | Bin 0 -> 12879 bytes .../__pycache__/caching.cpython-37.opt-1.pyc | Bin 0 -> 12978 bytes .../__pycache__/cptools.cpython-37.opt-1.pyc | Bin 0 -> 19739 bytes .../__pycache__/encoding.cpython-37.opt-1.pyc | Bin 0 -> 9995 bytes .../__pycache__/httputil.cpython-37.opt-1.pyc | Bin 0 -> 17806 bytes .../jsontools.cpython-37.opt-1.pyc | Bin 0 -> 3286 bytes .../__pycache__/locking.cpython-37.opt-1.pyc | Bin 0 -> 2106 bytes .../__pycache__/reprconf.cpython-37.opt-1.pyc | Bin 0 -> 15457 bytes .../__pycache__/sessions.cpython-37.opt-1.pyc | Bin 0 -> 26067 bytes .../__pycache__/static.cpython-37.opt-1.pyc | Bin 0 -> 9431 bytes .../xmlrpcutil.cpython-37.opt-1.pyc | Bin 0 -> 1760 bytes resources/lib/cherrypy/lib/auth_basic.py | 120 + resources/lib/cherrypy/lib/auth_digest.py | 464 +++ resources/lib/cherrypy/lib/caching.py | 482 +++ resources/lib/cherrypy/lib/covercp.py | 391 ++ resources/lib/cherrypy/lib/cpstats.py | 696 ++++ resources/lib/cherrypy/lib/cptools.py | 640 ++++ resources/lib/cherrypy/lib/encoding.py | 436 +++ resources/lib/cherrypy/lib/gctools.py | 218 ++ resources/lib/cherrypy/lib/httputil.py | 582 +++ resources/lib/cherrypy/lib/jsontools.py | 88 + resources/lib/cherrypy/lib/locking.py | 47 + resources/lib/cherrypy/lib/profiler.py | 221 ++ resources/lib/cherrypy/lib/reprconf.py | 516 +++ resources/lib/cherrypy/lib/sessions.py | 919 +++++ resources/lib/cherrypy/lib/static.py | 390 ++ resources/lib/cherrypy/lib/xmlrpcutil.py | 61 + resources/lib/cherrypy/process/__init__.py | 17 + .../__pycache__/__init__.cpython-37.opt-1.pyc | Bin 0 -> 769 bytes .../__pycache__/plugins.cpython-37.opt-1.pyc | Bin 0 -> 22629 bytes .../__pycache__/servers.cpython-37.opt-1.pyc | Bin 0 -> 12081 bytes .../__pycache__/win32.cpython-37.opt-1.pyc | Bin 0 -> 6081 bytes .../__pycache__/wspbus.cpython-37.opt-1.pyc | Bin 0 -> 17238 bytes resources/lib/cherrypy/process/plugins.py | 752 ++++ resources/lib/cherrypy/process/servers.py | 416 +++ resources/lib/cherrypy/process/win32.py | 183 + resources/lib/cherrypy/process/wspbus.py | 590 ++++ resources/lib/cherrypy/scaffold/__init__.py | 63 + .../lib/cherrypy/scaffold/apache-fcgi.conf | 22 + resources/lib/cherrypy/scaffold/example.conf | 3 + resources/lib/cherrypy/scaffold/site.conf | 14 + .../static/made_with_cherrypy_small.png | Bin 0 -> 6347 bytes resources/lib/cherrypy/test/__init__.py | 24 + .../lib/cherrypy/test/_test_decorators.py | 39 + .../lib/cherrypy/test/_test_states_demo.py | 69 + resources/lib/cherrypy/test/benchmark.py | 425 +++ resources/lib/cherrypy/test/checkerdemo.py | 49 + resources/lib/cherrypy/test/fastcgi.conf | 18 + resources/lib/cherrypy/test/fcgi.conf | 14 + resources/lib/cherrypy/test/helper.py | 542 +++ resources/lib/cherrypy/test/logtest.py | 228 ++ resources/lib/cherrypy/test/modfastcgi.py | 136 + resources/lib/cherrypy/test/modfcgid.py | 124 + resources/lib/cherrypy/test/modpy.py | 164 + resources/lib/cherrypy/test/modwsgi.py | 154 + resources/lib/cherrypy/test/sessiondemo.py | 161 + resources/lib/cherrypy/test/static/404.html | 5 + .../lib/cherrypy/test/static/dirback.jpg | Bin 0 -> 16585 bytes resources/lib/cherrypy/test/static/index.html | 1 + resources/lib/cherrypy/test/style.css | 1 + resources/lib/cherrypy/test/test.pem | 38 + .../lib/cherrypy/test/test_auth_basic.py | 135 + .../lib/cherrypy/test/test_auth_digest.py | 134 + resources/lib/cherrypy/test/test_bus.py | 274 ++ resources/lib/cherrypy/test/test_caching.py | 392 ++ resources/lib/cherrypy/test/test_compat.py | 34 + resources/lib/cherrypy/test/test_config.py | 303 ++ .../lib/cherrypy/test/test_config_server.py | 126 + resources/lib/cherrypy/test/test_conn.py | 873 +++++ resources/lib/cherrypy/test/test_core.py | 823 +++++ .../test/test_dynamicobjectmapping.py | 424 +++ resources/lib/cherrypy/test/test_encoding.py | 433 +++ resources/lib/cherrypy/test/test_etags.py | 84 + resources/lib/cherrypy/test/test_http.py | 307 ++ resources/lib/cherrypy/test/test_httputil.py | 80 + resources/lib/cherrypy/test/test_iterator.py | 196 + resources/lib/cherrypy/test/test_json.py | 102 + resources/lib/cherrypy/test/test_logging.py | 209 ++ resources/lib/cherrypy/test/test_mime.py | 134 + .../lib/cherrypy/test/test_misc_tools.py | 210 ++ resources/lib/cherrypy/test/test_native.py | 38 + .../lib/cherrypy/test/test_objectmapping.py | 430 +++ resources/lib/cherrypy/test/test_params.py | 61 + resources/lib/cherrypy/test/test_plugins.py | 14 + resources/lib/cherrypy/test/test_proxy.py | 154 + resources/lib/cherrypy/test/test_refleaks.py | 66 + .../lib/cherrypy/test/test_request_obj.py | 932 +++++ resources/lib/cherrypy/test/test_routes.py | 80 + resources/lib/cherrypy/test/test_session.py | 512 +++ .../cherrypy/test/test_sessionauthenticate.py | 61 + resources/lib/cherrypy/test/test_states.py | 473 +++ resources/lib/cherrypy/test/test_static.py | 438 +++ resources/lib/cherrypy/test/test_tools.py | 468 +++ resources/lib/cherrypy/test/test_tutorials.py | 210 ++ .../lib/cherrypy/test/test_virtualhost.py | 113 + resources/lib/cherrypy/test/test_wsgi_ns.py | 93 + .../cherrypy/test/test_wsgi_unix_socket.py | 93 + .../lib/cherrypy/test/test_wsgi_vhost.py | 35 + resources/lib/cherrypy/test/test_wsgiapps.py | 120 + resources/lib/cherrypy/test/test_xmlrpc.py | 183 + resources/lib/cherrypy/test/webtest.py | 11 + resources/lib/cherrypy/tutorial/README.rst | 16 + resources/lib/cherrypy/tutorial/__init__.py | 3 + .../lib/cherrypy/tutorial/custom_error.html | 14 + resources/lib/cherrypy/tutorial/pdf_file.pdf | Bin 0 -> 85698 bytes .../lib/cherrypy/tutorial/tut01_helloworld.py | 34 + .../cherrypy/tutorial/tut02_expose_methods.py | 32 + .../cherrypy/tutorial/tut03_get_and_post.py | 51 + .../cherrypy/tutorial/tut04_complex_site.py | 103 + .../tutorial/tut05_derived_objects.py | 80 + .../cherrypy/tutorial/tut06_default_method.py | 61 + .../lib/cherrypy/tutorial/tut07_sessions.py | 41 + .../tutorial/tut08_generators_and_yield.py | 44 + .../lib/cherrypy/tutorial/tut09_files.py | 105 + .../cherrypy/tutorial/tut10_http_errors.py | 84 + resources/lib/cherrypy/tutorial/tutorial.conf | 4 + resources/lib/contextlib2.py | 436 +++ resources/lib/fire/__init__.py | 24 + resources/lib/fire/__main__.py | 34 + .../__pycache__/__init__.cpython-37.opt-1.pyc | Bin 0 -> 423 bytes .../completion.cpython-37.opt-1.pyc | Bin 0 -> 13764 bytes .../__pycache__/core.cpython-37.opt-1.pyc | Bin 0 -> 25562 bytes .../custom_descriptions.cpython-37.opt-1.pyc | Bin 0 -> 4511 bytes .../decorators.cpython-37.opt-1.pyc | Bin 0 -> 2901 bytes .../docstrings.cpython-37.opt-1.pyc | Bin 0 -> 19848 bytes .../formatting.cpython-37.opt-1.pyc | Bin 0 -> 2536 bytes .../formatting_windows.cpython-37.opt-1.pyc | Bin 0 -> 1252 bytes .../__pycache__/helptext.cpython-37.opt-1.pyc | Bin 0 -> 17412 bytes .../inspectutils.cpython-37.opt-1.pyc | Bin 0 -> 8883 bytes .../__pycache__/interact.cpython-37.opt-1.pyc | Bin 0 -> 2729 bytes .../__pycache__/parser.cpython-37.opt-1.pyc | Bin 0 -> 3785 bytes .../__pycache__/trace.cpython-37.opt-1.pyc | Bin 0 -> 10983 bytes .../value_types.cpython-37.opt-1.pyc | Bin 0 -> 2435 bytes resources/lib/fire/completion.py | 516 +++ resources/lib/fire/completion_test.py | 193 + resources/lib/fire/console/__init__.py | 0 .../__pycache__/__init__.cpython-37.opt-1.pyc | Bin 0 -> 187 bytes .../console_attr.cpython-37.opt-1.pyc | Bin 0 -> 21737 bytes .../console_attr_os.cpython-37.opt-1.pyc | Bin 0 -> 5898 bytes .../console_io.cpython-37.opt-1.pyc | Bin 0 -> 2478 bytes .../console_pager.cpython-37.opt-1.pyc | Bin 0 -> 6333 bytes .../__pycache__/encoding.cpython-37.opt-1.pyc | Bin 0 -> 3574 bytes .../__pycache__/files.cpython-37.opt-1.pyc | Bin 0 -> 3247 bytes .../platforms.cpython-37.opt-1.pyc | Bin 0 -> 15411 bytes .../__pycache__/text.cpython-37.opt-1.pyc | Bin 0 -> 3399 bytes resources/lib/fire/console/console_attr.py | 813 +++++ resources/lib/fire/console/console_attr_os.py | 260 ++ resources/lib/fire/console/console_io.py | 114 + resources/lib/fire/console/console_pager.py | 299 ++ resources/lib/fire/console/encoding.py | 207 ++ resources/lib/fire/console/files.py | 116 + resources/lib/fire/console/platforms.py | 481 +++ resources/lib/fire/console/text.py | 103 + resources/lib/fire/core.py | 974 +++++ resources/lib/fire/core_test.py | 211 ++ resources/lib/fire/custom_descriptions.py | 151 + .../lib/fire/custom_descriptions_test.py | 73 + resources/lib/fire/decorators.py | 99 + resources/lib/fire/decorators_test.py | 174 + resources/lib/fire/docstrings.py | 760 ++++ resources/lib/fire/docstrings_fuzz_test.py | 40 + resources/lib/fire/docstrings_test.py | 284 ++ resources/lib/fire/fire_import_test.py | 40 + resources/lib/fire/fire_test.py | 726 ++++ resources/lib/fire/formatting.py | 97 + resources/lib/fire/formatting_test.py | 82 + resources/lib/fire/formatting_windows.py | 60 + resources/lib/fire/helptext.py | 638 ++++ resources/lib/fire/helptext_test.py | 458 +++ resources/lib/fire/inspectutils.py | 362 ++ resources/lib/fire/inspectutils_test.py | 141 + resources/lib/fire/interact.py | 99 + resources/lib/fire/interact_test.py | 52 + resources/lib/fire/main_test.py | 42 + resources/lib/fire/parser.py | 130 + resources/lib/fire/parser_fuzz_test.py | 99 + resources/lib/fire/parser_test.py | 143 + resources/lib/fire/test_components.py | 534 +++ resources/lib/fire/test_components_bin.py | 32 + resources/lib/fire/test_components_py3.py | 48 + resources/lib/fire/test_components_test.py | 40 + resources/lib/fire/testutils.py | 102 + resources/lib/fire/testutils_test.py | 59 + resources/lib/fire/trace.py | 315 ++ resources/lib/fire/trace_test.py | 154 + resources/lib/fire/value_types.py | 85 + resources/lib/jaraco/__init__.py | 1 + .../__pycache__/__init__.cpython-37.opt-1.pyc | Bin 0 -> 248 bytes .../collections.cpython-37.opt-1.pyc | Bin 0 -> 28290 bytes .../functools.cpython-37.opt-1.pyc | Bin 0 -> 13763 bytes resources/lib/jaraco/classes/__init__.py | 0 .../__pycache__/__init__.cpython-37.opt-1.pyc | Bin 0 -> 189 bytes .../properties.cpython-37.opt-1.pyc | Bin 0 -> 4701 bytes resources/lib/jaraco/classes/ancestry.py | 68 + resources/lib/jaraco/classes/meta.py | 64 + resources/lib/jaraco/classes/properties.py | 169 + resources/lib/jaraco/collections.py | 963 +++++ resources/lib/jaraco/functools.py | 458 +++ resources/lib/jaraco/text/Lorem ipsum.txt | 2 + resources/lib/jaraco/text/__init__.py | 500 +++ .../__pycache__/__init__.cpython-37.opt-1.pyc | Bin 0 -> 16639 bytes resources/lib/logless/__init__.py | 10 + .../__pycache__/__init__.cpython-37.opt-1.pyc | Bin 0 -> 394 bytes .../__pycache__/main.cpython-37.opt-1.pyc | Bin 0 -> 11099 bytes resources/lib/logless/main.py | 422 +++ resources/lib/more_itertools/__init__.py | 4 + .../__pycache__/__init__.cpython-37.opt-1.pyc | Bin 0 -> 255 bytes .../__pycache__/more.cpython-37.opt-1.pyc | Bin 0 -> 91754 bytes .../__pycache__/recipes.cpython-37.opt-1.pyc | Bin 0 -> 16701 bytes resources/lib/more_itertools/more.py | 3143 +++++++++++++++++ resources/lib/more_itertools/recipes.py | 572 +++ .../lib/more_itertools/tests/__init__.py | 0 .../lib/more_itertools/tests/test_more.py | 2074 +++++++++++ .../lib/more_itertools/tests/test_recipes.py | 616 ++++ resources/lib/pkg_resources.py | 27 + resources/lib/portend.py | 212 ++ resources/lib/runnow/__init__.py | 1 + .../__pycache__/__init__.cpython-37.opt-1.pyc | Bin 0 -> 220 bytes .../__pycache__/jobs.cpython-37.opt-1.pyc | Bin 0 -> 3220 bytes resources/lib/runnow/jobs.py | 126 + resources/lib/tempora/__init__.py | 505 +++ .../__pycache__/__init__.cpython-37.opt-1.pyc | Bin 0 -> 15785 bytes .../__pycache__/timing.cpython-37.opt-1.pyc | Bin 0 -> 6276 bytes resources/lib/tempora/schedule.py | 202 ++ resources/lib/tempora/tests/test_schedule.py | 118 + resources/lib/tempora/timing.py | 219 ++ resources/lib/tempora/utc.py | 36 + resources/lib/termcolor.py | 168 + resources/lib/zc/__init__.py | 1 + .../__pycache__/__init__.cpython-37.opt-1.pyc | Bin 0 -> 197 bytes resources/lib/zc/lockfile/README.txt | 70 + resources/lib/zc/lockfile/__init__.py | 104 + .../__pycache__/__init__.cpython-37.opt-1.pyc | Bin 0 -> 2854 bytes resources/lib/zc/lockfile/tests.py | 193 + 304 files changed, 57219 insertions(+) create mode 100644 resources/lib/backports/__init__.py create mode 100644 resources/lib/backports/functools_lru_cache.py create mode 100644 resources/lib/cheroot/__init__.py create mode 100644 resources/lib/cheroot/__main__.py create mode 100644 resources/lib/cheroot/__pycache__/__init__.cpython-37.opt-1.pyc create mode 100644 resources/lib/cheroot/__pycache__/_compat.cpython-37.opt-1.pyc create mode 100644 resources/lib/cheroot/__pycache__/errors.cpython-37.opt-1.pyc create mode 100644 resources/lib/cheroot/__pycache__/makefile.cpython-37.opt-1.pyc create mode 100644 resources/lib/cheroot/__pycache__/server.cpython-37.opt-1.pyc create mode 100644 resources/lib/cheroot/_compat.py create mode 100644 resources/lib/cheroot/cli.py create mode 100644 resources/lib/cheroot/errors.py create mode 100644 resources/lib/cheroot/makefile.py create mode 100644 resources/lib/cheroot/server.py create mode 100644 resources/lib/cheroot/ssl/__init__.py create mode 100644 resources/lib/cheroot/ssl/builtin.py create mode 100644 resources/lib/cheroot/ssl/pyopenssl.py create mode 100644 resources/lib/cheroot/test/__init__.py create mode 100644 resources/lib/cheroot/test/conftest.py create mode 100644 resources/lib/cheroot/test/helper.py create mode 100644 resources/lib/cheroot/test/test.pem create mode 100644 resources/lib/cheroot/test/test__compat.py create mode 100644 resources/lib/cheroot/test/test_conn.py create mode 100644 resources/lib/cheroot/test/test_core.py create mode 100644 resources/lib/cheroot/test/test_server.py create mode 100644 resources/lib/cheroot/test/webtest.py create mode 100644 resources/lib/cheroot/testing.py create mode 100644 resources/lib/cheroot/workers/__init__.py create mode 100644 resources/lib/cheroot/workers/__pycache__/__init__.cpython-37.opt-1.pyc create mode 100644 resources/lib/cheroot/workers/__pycache__/threadpool.cpython-37.opt-1.pyc create mode 100644 resources/lib/cheroot/workers/threadpool.py create mode 100644 resources/lib/cheroot/wsgi.py create mode 100644 resources/lib/cherrypy/__init__.py create mode 100644 resources/lib/cherrypy/__main__.py create mode 100644 resources/lib/cherrypy/__pycache__/__init__.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/__pycache__/_cpchecker.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/__pycache__/_cpcompat.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/__pycache__/_cpconfig.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/__pycache__/_cpdispatch.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/__pycache__/_cperror.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/__pycache__/_cplogging.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/__pycache__/_cpnative_server.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/__pycache__/_cpreqbody.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/__pycache__/_cprequest.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/__pycache__/_cpserver.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/__pycache__/_cptools.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/__pycache__/_cptree.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/__pycache__/_cpwsgi.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/__pycache__/_helper.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/_cpchecker.py create mode 100644 resources/lib/cherrypy/_cpcompat.py create mode 100644 resources/lib/cherrypy/_cpconfig.py create mode 100644 resources/lib/cherrypy/_cpdispatch.py create mode 100644 resources/lib/cherrypy/_cperror.py create mode 100644 resources/lib/cherrypy/_cplogging.py create mode 100644 resources/lib/cherrypy/_cpmodpy.py create mode 100644 resources/lib/cherrypy/_cpnative_server.py create mode 100644 resources/lib/cherrypy/_cpreqbody.py create mode 100644 resources/lib/cherrypy/_cprequest.py create mode 100644 resources/lib/cherrypy/_cpserver.py create mode 100644 resources/lib/cherrypy/_cptools.py create mode 100644 resources/lib/cherrypy/_cptree.py create mode 100644 resources/lib/cherrypy/_cpwsgi.py create mode 100644 resources/lib/cherrypy/_cpwsgi_server.py create mode 100644 resources/lib/cherrypy/_helper.py create mode 100644 resources/lib/cherrypy/daemon.py create mode 100644 resources/lib/cherrypy/favicon.ico create mode 100644 resources/lib/cherrypy/lib/__init__.py create mode 100644 resources/lib/cherrypy/lib/__pycache__/__init__.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/lib/__pycache__/auth_basic.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/lib/__pycache__/auth_digest.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/lib/__pycache__/caching.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/lib/__pycache__/cptools.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/lib/__pycache__/encoding.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/lib/__pycache__/httputil.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/lib/__pycache__/jsontools.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/lib/__pycache__/locking.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/lib/__pycache__/reprconf.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/lib/__pycache__/sessions.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/lib/__pycache__/static.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/lib/__pycache__/xmlrpcutil.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/lib/auth_basic.py create mode 100644 resources/lib/cherrypy/lib/auth_digest.py create mode 100644 resources/lib/cherrypy/lib/caching.py create mode 100644 resources/lib/cherrypy/lib/covercp.py create mode 100644 resources/lib/cherrypy/lib/cpstats.py create mode 100644 resources/lib/cherrypy/lib/cptools.py create mode 100644 resources/lib/cherrypy/lib/encoding.py create mode 100644 resources/lib/cherrypy/lib/gctools.py create mode 100644 resources/lib/cherrypy/lib/httputil.py create mode 100644 resources/lib/cherrypy/lib/jsontools.py create mode 100644 resources/lib/cherrypy/lib/locking.py create mode 100644 resources/lib/cherrypy/lib/profiler.py create mode 100644 resources/lib/cherrypy/lib/reprconf.py create mode 100644 resources/lib/cherrypy/lib/sessions.py create mode 100644 resources/lib/cherrypy/lib/static.py create mode 100644 resources/lib/cherrypy/lib/xmlrpcutil.py create mode 100644 resources/lib/cherrypy/process/__init__.py create mode 100644 resources/lib/cherrypy/process/__pycache__/__init__.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/process/__pycache__/plugins.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/process/__pycache__/servers.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/process/__pycache__/win32.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/process/__pycache__/wspbus.cpython-37.opt-1.pyc create mode 100644 resources/lib/cherrypy/process/plugins.py create mode 100644 resources/lib/cherrypy/process/servers.py create mode 100644 resources/lib/cherrypy/process/win32.py create mode 100644 resources/lib/cherrypy/process/wspbus.py create mode 100644 resources/lib/cherrypy/scaffold/__init__.py create mode 100644 resources/lib/cherrypy/scaffold/apache-fcgi.conf create mode 100644 resources/lib/cherrypy/scaffold/example.conf create mode 100644 resources/lib/cherrypy/scaffold/site.conf create mode 100644 resources/lib/cherrypy/scaffold/static/made_with_cherrypy_small.png create mode 100644 resources/lib/cherrypy/test/__init__.py create mode 100644 resources/lib/cherrypy/test/_test_decorators.py create mode 100644 resources/lib/cherrypy/test/_test_states_demo.py create mode 100644 resources/lib/cherrypy/test/benchmark.py create mode 100644 resources/lib/cherrypy/test/checkerdemo.py create mode 100644 resources/lib/cherrypy/test/fastcgi.conf create mode 100644 resources/lib/cherrypy/test/fcgi.conf create mode 100644 resources/lib/cherrypy/test/helper.py create mode 100644 resources/lib/cherrypy/test/logtest.py create mode 100644 resources/lib/cherrypy/test/modfastcgi.py create mode 100644 resources/lib/cherrypy/test/modfcgid.py create mode 100644 resources/lib/cherrypy/test/modpy.py create mode 100644 resources/lib/cherrypy/test/modwsgi.py create mode 100644 resources/lib/cherrypy/test/sessiondemo.py create mode 100644 resources/lib/cherrypy/test/static/404.html create mode 100644 resources/lib/cherrypy/test/static/dirback.jpg create mode 100644 resources/lib/cherrypy/test/static/index.html create mode 100644 resources/lib/cherrypy/test/style.css create mode 100644 resources/lib/cherrypy/test/test.pem create mode 100644 resources/lib/cherrypy/test/test_auth_basic.py create mode 100644 resources/lib/cherrypy/test/test_auth_digest.py create mode 100644 resources/lib/cherrypy/test/test_bus.py create mode 100644 resources/lib/cherrypy/test/test_caching.py create mode 100644 resources/lib/cherrypy/test/test_compat.py create mode 100644 resources/lib/cherrypy/test/test_config.py create mode 100644 resources/lib/cherrypy/test/test_config_server.py create mode 100644 resources/lib/cherrypy/test/test_conn.py create mode 100644 resources/lib/cherrypy/test/test_core.py create mode 100644 resources/lib/cherrypy/test/test_dynamicobjectmapping.py create mode 100644 resources/lib/cherrypy/test/test_encoding.py create mode 100644 resources/lib/cherrypy/test/test_etags.py create mode 100644 resources/lib/cherrypy/test/test_http.py create mode 100644 resources/lib/cherrypy/test/test_httputil.py create mode 100644 resources/lib/cherrypy/test/test_iterator.py create mode 100644 resources/lib/cherrypy/test/test_json.py create mode 100644 resources/lib/cherrypy/test/test_logging.py create mode 100644 resources/lib/cherrypy/test/test_mime.py create mode 100644 resources/lib/cherrypy/test/test_misc_tools.py create mode 100644 resources/lib/cherrypy/test/test_native.py create mode 100644 resources/lib/cherrypy/test/test_objectmapping.py create mode 100644 resources/lib/cherrypy/test/test_params.py create mode 100644 resources/lib/cherrypy/test/test_plugins.py create mode 100644 resources/lib/cherrypy/test/test_proxy.py create mode 100644 resources/lib/cherrypy/test/test_refleaks.py create mode 100644 resources/lib/cherrypy/test/test_request_obj.py create mode 100644 resources/lib/cherrypy/test/test_routes.py create mode 100644 resources/lib/cherrypy/test/test_session.py create mode 100644 resources/lib/cherrypy/test/test_sessionauthenticate.py create mode 100644 resources/lib/cherrypy/test/test_states.py create mode 100644 resources/lib/cherrypy/test/test_static.py create mode 100644 resources/lib/cherrypy/test/test_tools.py create mode 100644 resources/lib/cherrypy/test/test_tutorials.py create mode 100644 resources/lib/cherrypy/test/test_virtualhost.py create mode 100644 resources/lib/cherrypy/test/test_wsgi_ns.py create mode 100644 resources/lib/cherrypy/test/test_wsgi_unix_socket.py create mode 100644 resources/lib/cherrypy/test/test_wsgi_vhost.py create mode 100644 resources/lib/cherrypy/test/test_wsgiapps.py create mode 100644 resources/lib/cherrypy/test/test_xmlrpc.py create mode 100644 resources/lib/cherrypy/test/webtest.py create mode 100644 resources/lib/cherrypy/tutorial/README.rst create mode 100644 resources/lib/cherrypy/tutorial/__init__.py create mode 100644 resources/lib/cherrypy/tutorial/custom_error.html create mode 100644 resources/lib/cherrypy/tutorial/pdf_file.pdf create mode 100644 resources/lib/cherrypy/tutorial/tut01_helloworld.py create mode 100644 resources/lib/cherrypy/tutorial/tut02_expose_methods.py create mode 100644 resources/lib/cherrypy/tutorial/tut03_get_and_post.py create mode 100644 resources/lib/cherrypy/tutorial/tut04_complex_site.py create mode 100644 resources/lib/cherrypy/tutorial/tut05_derived_objects.py create mode 100644 resources/lib/cherrypy/tutorial/tut06_default_method.py create mode 100644 resources/lib/cherrypy/tutorial/tut07_sessions.py create mode 100644 resources/lib/cherrypy/tutorial/tut08_generators_and_yield.py create mode 100644 resources/lib/cherrypy/tutorial/tut09_files.py create mode 100644 resources/lib/cherrypy/tutorial/tut10_http_errors.py create mode 100644 resources/lib/cherrypy/tutorial/tutorial.conf create mode 100644 resources/lib/contextlib2.py create mode 100644 resources/lib/fire/__init__.py create mode 100644 resources/lib/fire/__main__.py create mode 100644 resources/lib/fire/__pycache__/__init__.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/__pycache__/completion.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/__pycache__/core.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/__pycache__/custom_descriptions.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/__pycache__/decorators.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/__pycache__/docstrings.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/__pycache__/formatting.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/__pycache__/formatting_windows.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/__pycache__/helptext.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/__pycache__/inspectutils.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/__pycache__/interact.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/__pycache__/parser.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/__pycache__/trace.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/__pycache__/value_types.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/completion.py create mode 100644 resources/lib/fire/completion_test.py create mode 100644 resources/lib/fire/console/__init__.py create mode 100644 resources/lib/fire/console/__pycache__/__init__.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/console/__pycache__/console_attr.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/console/__pycache__/console_attr_os.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/console/__pycache__/console_io.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/console/__pycache__/console_pager.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/console/__pycache__/encoding.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/console/__pycache__/files.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/console/__pycache__/platforms.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/console/__pycache__/text.cpython-37.opt-1.pyc create mode 100644 resources/lib/fire/console/console_attr.py create mode 100644 resources/lib/fire/console/console_attr_os.py create mode 100644 resources/lib/fire/console/console_io.py create mode 100644 resources/lib/fire/console/console_pager.py create mode 100644 resources/lib/fire/console/encoding.py create mode 100644 resources/lib/fire/console/files.py create mode 100644 resources/lib/fire/console/platforms.py create mode 100644 resources/lib/fire/console/text.py create mode 100644 resources/lib/fire/core.py create mode 100644 resources/lib/fire/core_test.py create mode 100644 resources/lib/fire/custom_descriptions.py create mode 100644 resources/lib/fire/custom_descriptions_test.py create mode 100644 resources/lib/fire/decorators.py create mode 100644 resources/lib/fire/decorators_test.py create mode 100644 resources/lib/fire/docstrings.py create mode 100644 resources/lib/fire/docstrings_fuzz_test.py create mode 100644 resources/lib/fire/docstrings_test.py create mode 100644 resources/lib/fire/fire_import_test.py create mode 100644 resources/lib/fire/fire_test.py create mode 100644 resources/lib/fire/formatting.py create mode 100644 resources/lib/fire/formatting_test.py create mode 100644 resources/lib/fire/formatting_windows.py create mode 100644 resources/lib/fire/helptext.py create mode 100644 resources/lib/fire/helptext_test.py create mode 100644 resources/lib/fire/inspectutils.py create mode 100644 resources/lib/fire/inspectutils_test.py create mode 100644 resources/lib/fire/interact.py create mode 100644 resources/lib/fire/interact_test.py create mode 100644 resources/lib/fire/main_test.py create mode 100644 resources/lib/fire/parser.py create mode 100644 resources/lib/fire/parser_fuzz_test.py create mode 100644 resources/lib/fire/parser_test.py create mode 100644 resources/lib/fire/test_components.py create mode 100644 resources/lib/fire/test_components_bin.py create mode 100644 resources/lib/fire/test_components_py3.py create mode 100644 resources/lib/fire/test_components_test.py create mode 100644 resources/lib/fire/testutils.py create mode 100644 resources/lib/fire/testutils_test.py create mode 100644 resources/lib/fire/trace.py create mode 100644 resources/lib/fire/trace_test.py create mode 100644 resources/lib/fire/value_types.py create mode 100644 resources/lib/jaraco/__init__.py create mode 100644 resources/lib/jaraco/__pycache__/__init__.cpython-37.opt-1.pyc create mode 100644 resources/lib/jaraco/__pycache__/collections.cpython-37.opt-1.pyc create mode 100644 resources/lib/jaraco/__pycache__/functools.cpython-37.opt-1.pyc create mode 100644 resources/lib/jaraco/classes/__init__.py create mode 100644 resources/lib/jaraco/classes/__pycache__/__init__.cpython-37.opt-1.pyc create mode 100644 resources/lib/jaraco/classes/__pycache__/properties.cpython-37.opt-1.pyc create mode 100644 resources/lib/jaraco/classes/ancestry.py create mode 100644 resources/lib/jaraco/classes/meta.py create mode 100644 resources/lib/jaraco/classes/properties.py create mode 100644 resources/lib/jaraco/collections.py create mode 100644 resources/lib/jaraco/functools.py create mode 100644 resources/lib/jaraco/text/Lorem ipsum.txt create mode 100644 resources/lib/jaraco/text/__init__.py create mode 100644 resources/lib/jaraco/text/__pycache__/__init__.cpython-37.opt-1.pyc create mode 100644 resources/lib/logless/__init__.py create mode 100644 resources/lib/logless/__pycache__/__init__.cpython-37.opt-1.pyc create mode 100644 resources/lib/logless/__pycache__/main.cpython-37.opt-1.pyc create mode 100644 resources/lib/logless/main.py create mode 100644 resources/lib/more_itertools/__init__.py create mode 100644 resources/lib/more_itertools/__pycache__/__init__.cpython-37.opt-1.pyc create mode 100644 resources/lib/more_itertools/__pycache__/more.cpython-37.opt-1.pyc create mode 100644 resources/lib/more_itertools/__pycache__/recipes.cpython-37.opt-1.pyc create mode 100644 resources/lib/more_itertools/more.py create mode 100644 resources/lib/more_itertools/recipes.py create mode 100644 resources/lib/more_itertools/tests/__init__.py create mode 100644 resources/lib/more_itertools/tests/test_more.py create mode 100644 resources/lib/more_itertools/tests/test_recipes.py create mode 100644 resources/lib/pkg_resources.py create mode 100644 resources/lib/portend.py create mode 100644 resources/lib/runnow/__init__.py create mode 100644 resources/lib/runnow/__pycache__/__init__.cpython-37.opt-1.pyc create mode 100644 resources/lib/runnow/__pycache__/jobs.cpython-37.opt-1.pyc create mode 100644 resources/lib/runnow/jobs.py create mode 100644 resources/lib/tempora/__init__.py create mode 100644 resources/lib/tempora/__pycache__/__init__.cpython-37.opt-1.pyc create mode 100644 resources/lib/tempora/__pycache__/timing.cpython-37.opt-1.pyc create mode 100644 resources/lib/tempora/schedule.py create mode 100644 resources/lib/tempora/tests/test_schedule.py create mode 100644 resources/lib/tempora/timing.py create mode 100644 resources/lib/tempora/utc.py create mode 100644 resources/lib/termcolor.py create mode 100644 resources/lib/zc/__init__.py create mode 100644 resources/lib/zc/__pycache__/__init__.cpython-37.opt-1.pyc create mode 100644 resources/lib/zc/lockfile/README.txt create mode 100644 resources/lib/zc/lockfile/__init__.py create mode 100644 resources/lib/zc/lockfile/__pycache__/__init__.cpython-37.opt-1.pyc create mode 100644 resources/lib/zc/lockfile/tests.py diff --git a/resources/lib/backports/__init__.py b/resources/lib/backports/__init__.py new file mode 100644 index 0000000..69e3be5 --- /dev/null +++ b/resources/lib/backports/__init__.py @@ -0,0 +1 @@ +__path__ = __import__('pkgutil').extend_path(__path__, __name__) diff --git a/resources/lib/backports/functools_lru_cache.py b/resources/lib/backports/functools_lru_cache.py new file mode 100644 index 0000000..707c6c7 --- /dev/null +++ b/resources/lib/backports/functools_lru_cache.py @@ -0,0 +1,184 @@ +from __future__ import absolute_import + +import functools +from collections import namedtuple +from threading import RLock + +_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"]) + + +@functools.wraps(functools.update_wrapper) +def update_wrapper(wrapper, + wrapped, + assigned = functools.WRAPPER_ASSIGNMENTS, + updated = functools.WRAPPER_UPDATES): + """ + Patch two bugs in functools.update_wrapper. + """ + # workaround for http://bugs.python.org/issue3445 + assigned = tuple(attr for attr in assigned if hasattr(wrapped, attr)) + wrapper = functools.update_wrapper(wrapper, wrapped, assigned, updated) + # workaround for https://bugs.python.org/issue17482 + wrapper.__wrapped__ = wrapped + return wrapper + + +class _HashedSeq(list): + __slots__ = 'hashvalue' + + def __init__(self, tup, hash=hash): + self[:] = tup + self.hashvalue = hash(tup) + + def __hash__(self): + return self.hashvalue + + +def _make_key(args, kwds, typed, + kwd_mark=(object(),), + fasttypes=set([int, str, frozenset, type(None)]), + sorted=sorted, tuple=tuple, type=type, len=len): + 'Make a cache key from optionally typed positional and keyword arguments' + key = args + if kwds: + sorted_items = sorted(kwds.items()) + key += kwd_mark + for item in sorted_items: + key += item + if typed: + key += tuple(type(v) for v in args) + if kwds: + key += tuple(type(v) for k, v in sorted_items) + elif len(key) == 1 and type(key[0]) in fasttypes: + return key[0] + return _HashedSeq(key) + + +def lru_cache(maxsize=100, typed=False): + """Least-recently-used cache decorator. + + If *maxsize* is set to None, the LRU features are disabled and the cache + can grow without bound. + + If *typed* is True, arguments of different types will be cached separately. + For example, f(3.0) and f(3) will be treated as distinct calls with + distinct results. + + Arguments to the cached function must be hashable. + + View the cache statistics named tuple (hits, misses, maxsize, currsize) with + f.cache_info(). Clear the cache and statistics with f.cache_clear(). + Access the underlying function with f.__wrapped__. + + See: http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used + + """ + + # Users should only access the lru_cache through its public API: + # cache_info, cache_clear, and f.__wrapped__ + # The internals of the lru_cache are encapsulated for thread safety and + # to allow the implementation to change (including a possible C version). + + def decorating_function(user_function): + + cache = dict() + stats = [0, 0] # make statistics updateable non-locally + HITS, MISSES = 0, 1 # names for the stats fields + make_key = _make_key + cache_get = cache.get # bound method to lookup key or return None + _len = len # localize the global len() function + lock = RLock() # because linkedlist updates aren't threadsafe + root = [] # root of the circular doubly linked list + root[:] = [root, root, None, None] # initialize by pointing to self + nonlocal_root = [root] # make updateable non-locally + PREV, NEXT, KEY, RESULT = 0, 1, 2, 3 # names for the link fields + + if maxsize == 0: + + def wrapper(*args, **kwds): + # no caching, just do a statistics update after a successful call + result = user_function(*args, **kwds) + stats[MISSES] += 1 + return result + + elif maxsize is None: + + def wrapper(*args, **kwds): + # simple caching without ordering or size limit + key = make_key(args, kwds, typed) + result = cache_get(key, root) # root used here as a unique not-found sentinel + if result is not root: + stats[HITS] += 1 + return result + result = user_function(*args, **kwds) + cache[key] = result + stats[MISSES] += 1 + return result + + else: + + def wrapper(*args, **kwds): + # size limited caching that tracks accesses by recency + key = make_key(args, kwds, typed) if kwds or typed else args + with lock: + link = cache_get(key) + if link is not None: + # record recent use of the key by moving it to the front of the list + root, = nonlocal_root + link_prev, link_next, key, result = link + link_prev[NEXT] = link_next + link_next[PREV] = link_prev + last = root[PREV] + last[NEXT] = root[PREV] = link + link[PREV] = last + link[NEXT] = root + stats[HITS] += 1 + return result + result = user_function(*args, **kwds) + with lock: + root, = nonlocal_root + if key in cache: + # getting here means that this same key was added to the + # cache while the lock was released. since the link + # update is already done, we need only return the + # computed result and update the count of misses. + pass + elif _len(cache) >= maxsize: + # use the old root to store the new key and result + oldroot = root + oldroot[KEY] = key + oldroot[RESULT] = result + # empty the oldest link and make it the new root + root = nonlocal_root[0] = oldroot[NEXT] + oldkey = root[KEY] + root[KEY] = root[RESULT] = None + # now update the cache dictionary for the new links + del cache[oldkey] + cache[key] = oldroot + else: + # put result in a new link at the front of the list + last = root[PREV] + link = [last, root, key, result] + last[NEXT] = root[PREV] = cache[key] = link + stats[MISSES] += 1 + return result + + def cache_info(): + """Report cache statistics""" + with lock: + return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache)) + + def cache_clear(): + """Clear the cache and cache statistics""" + with lock: + cache.clear() + root = nonlocal_root[0] + root[:] = [root, root, None, None] + stats[:] = [0, 0] + + wrapper.__wrapped__ = user_function + wrapper.cache_info = cache_info + wrapper.cache_clear = cache_clear + return update_wrapper(wrapper, user_function) + + return decorating_function diff --git a/resources/lib/cheroot/__init__.py b/resources/lib/cheroot/__init__.py new file mode 100644 index 0000000..a313660 --- /dev/null +++ b/resources/lib/cheroot/__init__.py @@ -0,0 +1,6 @@ +"""High-performance, pure-Python HTTP server used by CherryPy.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +__version__ = '6.4.0' diff --git a/resources/lib/cheroot/__main__.py b/resources/lib/cheroot/__main__.py new file mode 100644 index 0000000..d2e27c1 --- /dev/null +++ b/resources/lib/cheroot/__main__.py @@ -0,0 +1,6 @@ +"""Stub for accessing the Cheroot CLI tool.""" + +from .cli import main + +if __name__ == '__main__': + main() diff --git a/resources/lib/cheroot/__pycache__/__init__.cpython-37.opt-1.pyc b/resources/lib/cheroot/__pycache__/__init__.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86cafa9e8d3c1907bb8e2af2027bdc2d57f71783 GIT binary patch literal 405 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 BK}-Mu literal 0 HcmV?d00001 diff --git a/resources/lib/cheroot/__pycache__/server.cpython-37.opt-1.pyc b/resources/lib/cheroot/__pycache__/server.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..065c6fdd5ca9506a598855f5e92497b7dc0306d6 GIT binary patch literal 45642 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 I@V-v`|I+}V&j0`b literal 0 HcmV?d00001 diff --git a/resources/lib/cheroot/_compat.py b/resources/lib/cheroot/_compat.py new file mode 100644 index 0000000..e98f91f --- /dev/null +++ b/resources/lib/cheroot/_compat.py @@ -0,0 +1,66 @@ +"""Compatibility code for using Cheroot with various versions of Python.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import re + +import six + +if six.PY3: + def ntob(n, encoding='ISO-8859-1'): + """Return the native string as bytes in the given encoding.""" + assert_native(n) + # In Python 3, the native string type is unicode + return n.encode(encoding) + + def ntou(n, encoding='ISO-8859-1'): + """Return the native string as unicode with the given encoding.""" + assert_native(n) + # In Python 3, the native string type is unicode + return n + + def bton(b, encoding='ISO-8859-1'): + """Return the byte string as native string in the given encoding.""" + return b.decode(encoding) +else: + # Python 2 + def ntob(n, encoding='ISO-8859-1'): + """Return the native string as bytes in the given encoding.""" + assert_native(n) + # In Python 2, the native string type is bytes. Assume it's already + # in the given encoding, which for ISO-8859-1 is almost always what + # was intended. + return n + + def ntou(n, encoding='ISO-8859-1'): + """Return the native string as unicode with the given encoding.""" + assert_native(n) + # In Python 2, the native string type is bytes. + # First, check for the special encoding 'escape'. The test suite uses + # this to signal that it wants to pass a string with embedded \uXXXX + # escapes, but without having to prefix it with u'' for Python 2, + # but no prefix for Python 3. + if encoding == 'escape': + return six.u( + re.sub(r'\\u([0-9a-zA-Z]{4})', + lambda m: six.unichr(int(m.group(1), 16)), + n.decode('ISO-8859-1'))) + # Assume it's already in the given encoding, which for ISO-8859-1 + # is almost always what was intended. + return n.decode(encoding) + + def bton(b, encoding='ISO-8859-1'): + """Return the byte string as native string in the given encoding.""" + return b + + +def assert_native(n): + """Check whether the input is of nativ ``str`` type. + + Raises: + TypeError: in case of failed check + + """ + if not isinstance(n, str): + raise TypeError('n must be a native str (got %s)' % type(n).__name__) diff --git a/resources/lib/cheroot/cli.py b/resources/lib/cheroot/cli.py new file mode 100644 index 0000000..6d59fb5 --- /dev/null +++ b/resources/lib/cheroot/cli.py @@ -0,0 +1,233 @@ +"""Command line tool for starting a Cheroot WSGI/HTTP server instance. + +Basic usage:: + + # Start a server on 127.0.0.1:8000 with the default settings + # for the WSGI app myapp/wsgi.py:application() + cheroot myapp.wsgi + + # Start a server on 0.0.0.0:9000 with 8 threads + # for the WSGI app myapp/wsgi.py:main_app() + cheroot myapp.wsgi:main_app --bind 0.0.0.0:9000 --threads 8 + + # Start a server for the cheroot.server.Gateway subclass + # myapp/gateway.py:HTTPGateway + cheroot myapp.gateway:HTTPGateway + + # Start a server on the UNIX socket /var/spool/myapp.sock + cheroot myapp.wsgi --bind /var/spool/myapp.sock + + # Start a server on the abstract UNIX socket CherootServer + cheroot myapp.wsgi --bind @CherootServer +""" + +import argparse +from importlib import import_module +import os +import sys +import contextlib + +import six + +from . import server +from . import wsgi + + +__metaclass__ = type + + +class BindLocation: + """A class for storing the bind location for a Cheroot instance.""" + + +class TCPSocket(BindLocation): + """TCPSocket.""" + + def __init__(self, address, port): + """Initialize. + + Args: + address (str): Host name or IP address + port (int): TCP port number + """ + self.bind_addr = address, port + + +class UnixSocket(BindLocation): + """UnixSocket.""" + + def __init__(self, path): + """Initialize.""" + self.bind_addr = path + + +class AbstractSocket(BindLocation): + """AbstractSocket.""" + + def __init__(self, addr): + """Initialize.""" + self.bind_addr = '\0{}'.format(self.abstract_socket) + + +class Application: + """Application.""" + + @classmethod + def resolve(cls, full_path): + """Read WSGI app/Gateway path string and import application module.""" + mod_path, _, app_path = full_path.partition(':') + app = getattr(import_module(mod_path), app_path or 'application') + + with contextlib.suppress(TypeError): + if issubclass(app, server.Gateway): + return GatewayYo(app) + + return cls(app) + + def __init__(self, wsgi_app): + """Initialize.""" + if not callable(wsgi_app): + raise TypeError( + 'Application must be a callable object or ' + 'cheroot.server.Gateway subclass' + ) + self.wsgi_app = wsgi_app + + def server_args(self, parsed_args): + """Return keyword args for Server class.""" + args = { + arg: value + for arg, value in vars(parsed_args).items() + if not arg.startswith('_') and value is not None + } + args.update(vars(self)) + return args + + def server(self, parsed_args): + """Server.""" + return wsgi.Server(**self.server_args(parsed_args)) + + +class GatewayYo: + """Gateway.""" + + def __init__(self, gateway): + """Init.""" + self.gateway = gateway + + def server(self, parsed_args): + """Server.""" + server_args = vars(self) + server_args['bind_addr'] = parsed_args['bind_addr'] + if parsed_args.max is not None: + server_args['maxthreads'] = parsed_args.max + if parsed_args.numthreads is not None: + server_args['minthreads'] = parsed_args.numthreads + return server.HTTPServer(**server_args) + + +def parse_wsgi_bind_location(bind_addr_string): + """Convert bind address string to a BindLocation.""" + # try and match for an IP/hostname and port + match = six.moves.urllib.parse.urlparse('//{}'.format(bind_addr_string)) + try: + addr = match.hostname + port = match.port + if addr is not None or port is not None: + return TCPSocket(addr, port) + except ValueError: + pass + + # else, assume a UNIX socket path + # if the string begins with an @ symbol, use an abstract socket + if bind_addr_string.startswith('@'): + return AbstractSocket(bind_addr_string[1:]) + return UnixSocket(path=bind_addr_string) + + +def parse_wsgi_bind_addr(bind_addr_string): + """Convert bind address string to bind address parameter.""" + return parse_wsgi_bind_location(bind_addr_string).bind_addr + + +_arg_spec = { + '_wsgi_app': dict( + metavar='APP_MODULE', + type=Application.resolve, + help='WSGI application callable or cheroot.server.Gateway subclass', + ), + '--bind': dict( + metavar='ADDRESS', + dest='bind_addr', + type=parse_wsgi_bind_addr, + default='[::1]:8000', + help='Network interface to listen on (default: [::1]:8000)', + ), + '--chdir': dict( + metavar='PATH', + type=os.chdir, + help='Set the working directory', + ), + '--server-name': dict( + dest='server_name', + type=str, + help='Web server name to be advertised via Server HTTP header', + ), + '--threads': dict( + metavar='INT', + dest='numthreads', + type=int, + help='Minimum number of worker threads', + ), + '--max-threads': dict( + metavar='INT', + dest='max', + type=int, + help='Maximum number of worker threads', + ), + '--timeout': dict( + metavar='INT', + dest='timeout', + type=int, + help='Timeout in seconds for accepted connections', + ), + '--shutdown-timeout': dict( + metavar='INT', + dest='shutdown_timeout', + type=int, + help='Time in seconds to wait for worker threads to cleanly exit', + ), + '--request-queue-size': dict( + metavar='INT', + dest='request_queue_size', + type=int, + help='Maximum number of queued connections', + ), + '--accepted-queue-size': dict( + metavar='INT', + dest='accepted_queue_size', + type=int, + help='Maximum number of active requests in queue', + ), + '--accepted-queue-timeout': dict( + metavar='INT', + dest='accepted_queue_timeout', + type=int, + help='Timeout in seconds for putting requests into queue', + ), +} + + +def main(): + """Create a new Cheroot instance with arguments from the command line.""" + parser = argparse.ArgumentParser( + description='Start an instance of the Cheroot WSGI/HTTP server.') + for arg, spec in _arg_spec.items(): + parser.add_argument(arg, **spec) + raw_args = parser.parse_args() + + # ensure cwd in sys.path + '' in sys.path or sys.path.insert(0, '') + + # create a server based on the arguments provided + raw_args._wsgi_app.server(raw_args).safe_start() diff --git a/resources/lib/cheroot/errors.py b/resources/lib/cheroot/errors.py new file mode 100644 index 0000000..82412b4 --- /dev/null +++ b/resources/lib/cheroot/errors.py @@ -0,0 +1,58 @@ +"""Collection of exceptions raised and/or processed by Cheroot.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import errno +import sys + + +class MaxSizeExceeded(Exception): + """Exception raised when a client sends more data then acceptable within limit. + + Depends on ``request.body.maxbytes`` config option if used within CherryPy + """ + + +class NoSSLError(Exception): + """Exception raised when a client speaks HTTP to an HTTPS socket.""" + + +class FatalSSLAlert(Exception): + """Exception raised when the SSL implementation signals a fatal alert.""" + + +def plat_specific_errors(*errnames): + """Return error numbers for all errors in errnames on this platform. + + The 'errno' module contains different global constants depending on + the specific platform (OS). This function will return the list of + numeric values for a given list of potential names. + """ + errno_names = dir(errno) + nums = [getattr(errno, k) for k in errnames if k in errno_names] + # de-dupe the list + return list(dict.fromkeys(nums).keys()) + + +socket_error_eintr = plat_specific_errors('EINTR', 'WSAEINTR') + +socket_errors_to_ignore = plat_specific_errors( + 'EPIPE', + 'EBADF', 'WSAEBADF', + 'ENOTSOCK', 'WSAENOTSOCK', + 'ETIMEDOUT', 'WSAETIMEDOUT', + 'ECONNREFUSED', 'WSAECONNREFUSED', + 'ECONNRESET', 'WSAECONNRESET', + 'ECONNABORTED', 'WSAECONNABORTED', + 'ENETRESET', 'WSAENETRESET', + 'EHOSTDOWN', 'EHOSTUNREACH', +) +socket_errors_to_ignore.append('timed out') +socket_errors_to_ignore.append('The read operation timed out') +socket_errors_nonblocking = plat_specific_errors( + 'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK') + +if sys.platform == 'darwin': + socket_errors_to_ignore.extend(plat_specific_errors('EPROTOTYPE')) + socket_errors_nonblocking.extend(plat_specific_errors('EPROTOTYPE')) diff --git a/resources/lib/cheroot/makefile.py b/resources/lib/cheroot/makefile.py new file mode 100644 index 0000000..a76f2ed --- /dev/null +++ b/resources/lib/cheroot/makefile.py @@ -0,0 +1,387 @@ +"""Socket file object.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import socket + +try: + # prefer slower Python-based io module + import _pyio as io +except ImportError: + # Python 2.6 + import io + +import six + +from . import errors + + +class BufferedWriter(io.BufferedWriter): + """Faux file object attached to a socket object.""" + + def write(self, b): + """Write bytes to buffer.""" + self._checkClosed() + if isinstance(b, str): + raise TypeError("can't write str to binary stream") + + with self._write_lock: + self._write_buf.extend(b) + self._flush_unlocked() + return len(b) + + def _flush_unlocked(self): + self._checkClosed('flush of closed file') + while self._write_buf: + try: + # ssl sockets only except 'bytes', not bytearrays + # so perhaps we should conditionally wrap this for perf? + n = self.raw.write(bytes(self._write_buf)) + except io.BlockingIOError as e: + n = e.characters_written + del self._write_buf[:n] + + +def MakeFile_PY3(sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE): + """File object attached to a socket object.""" + if 'r' in mode: + return io.BufferedReader(socket.SocketIO(sock, mode), bufsize) + else: + return BufferedWriter(socket.SocketIO(sock, mode), bufsize) + + +class MakeFile_PY2(getattr(socket, '_fileobject', object)): + """Faux file object attached to a socket object.""" + + def __init__(self, *args, **kwargs): + """Initialize faux file object.""" + self.bytes_read = 0 + self.bytes_written = 0 + socket._fileobject.__init__(self, *args, **kwargs) + + def write(self, data): + """Sendall for non-blocking sockets.""" + while data: + try: + bytes_sent = self.send(data) + data = data[bytes_sent:] + except socket.error as e: + if e.args[0] not in errors.socket_errors_nonblocking: + raise + + def send(self, data): + """Send some part of message to the socket.""" + bytes_sent = self._sock.send(data) + self.bytes_written += bytes_sent + return bytes_sent + + def flush(self): + """Write all data from buffer to socket and reset write buffer.""" + if self._wbuf: + buffer = ''.join(self._wbuf) + self._wbuf = [] + self.write(buffer) + + def recv(self, size): + """Receive message of a size from the socket.""" + while True: + try: + data = self._sock.recv(size) + self.bytes_read += len(data) + return data + except socket.error as e: + what = ( + e.args[0] not in errors.socket_errors_nonblocking + and e.args[0] not in errors.socket_error_eintr + ) + if what: + raise + + class FauxSocket: + """Faux socket with the minimal interface required by pypy.""" + + def _reuse(self): + pass + + _fileobject_uses_str_type = six.PY2 and isinstance( + socket._fileobject(FauxSocket())._rbuf, six.string_types) + + # FauxSocket is no longer needed + del FauxSocket + + if not _fileobject_uses_str_type: + def read(self, size=-1): + """Read data from the socket to buffer.""" + # Use max, disallow tiny reads in a loop as they are very + # inefficient. + # We never leave read() with any leftover data from a new recv() + # call in our internal buffer. + rbufsize = max(self._rbufsize, self.default_bufsize) + # Our use of StringIO rather than lists of string objects returned + # by recv() minimizes memory usage and fragmentation that occurs + # when rbufsize is large compared to the typical return value of + # recv(). + buf = self._rbuf + buf.seek(0, 2) # seek end + if size < 0: + # Read until EOF + # reset _rbuf. we consume it via buf. + self._rbuf = io.BytesIO() + while True: + data = self.recv(rbufsize) + if not data: + break + buf.write(data) + return buf.getvalue() + else: + # Read until size bytes or EOF seen, whichever comes first + buf_len = buf.tell() + if buf_len >= size: + # Already have size bytes in our buffer? Extract and + # return. + buf.seek(0) + rv = buf.read(size) + self._rbuf = io.BytesIO() + self._rbuf.write(buf.read()) + return rv + + # reset _rbuf. we consume it via buf. + self._rbuf = io.BytesIO() + while True: + left = size - buf_len + # recv() will malloc the amount of memory given as its + # parameter even though it often returns much less data + # than that. The returned data string is short lived + # as we copy it into a StringIO and free it. This avoids + # fragmentation issues on many platforms. + data = self.recv(left) + if not data: + break + n = len(data) + if n == size and not buf_len: + # Shortcut. Avoid buffer data copies when: + # - We have no data in our buffer. + # AND + # - Our call to recv returned exactly the + # number of bytes we were asked to read. + return data + if n == left: + buf.write(data) + del data # explicit free + break + assert n <= left, 'recv(%d) returned %d bytes' % (left, n) + buf.write(data) + buf_len += n + del data # explicit free + # assert buf_len == buf.tell() + return buf.getvalue() + + def readline(self, size=-1): + """Read line from the socket to buffer.""" + buf = self._rbuf + buf.seek(0, 2) # seek end + if buf.tell() > 0: + # check if we already have it in our buffer + buf.seek(0) + bline = buf.readline(size) + if bline.endswith('\n') or len(bline) == size: + self._rbuf = io.BytesIO() + self._rbuf.write(buf.read()) + return bline + del bline + if size < 0: + # Read until \n or EOF, whichever comes first + if self._rbufsize <= 1: + # Speed up unbuffered case + buf.seek(0) + buffers = [buf.read()] + # reset _rbuf. we consume it via buf. + self._rbuf = io.BytesIO() + data = None + recv = self.recv + while data != '\n': + data = recv(1) + if not data: + break + buffers.append(data) + return ''.join(buffers) + + buf.seek(0, 2) # seek end + # reset _rbuf. we consume it via buf. + self._rbuf = io.BytesIO() + while True: + data = self.recv(self._rbufsize) + if not data: + break + nl = data.find('\n') + if nl >= 0: + nl += 1 + buf.write(data[:nl]) + self._rbuf.write(data[nl:]) + del data + break + buf.write(data) + return buf.getvalue() + else: + # Read until size bytes or \n or EOF seen, whichever comes + # first + buf.seek(0, 2) # seek end + buf_len = buf.tell() + if buf_len >= size: + buf.seek(0) + rv = buf.read(size) + self._rbuf = io.BytesIO() + self._rbuf.write(buf.read()) + return rv + # reset _rbuf. we consume it via buf. + self._rbuf = io.BytesIO() + while True: + data = self.recv(self._rbufsize) + if not data: + break + left = size - buf_len + # did we just receive a newline? + nl = data.find('\n', 0, left) + if nl >= 0: + nl += 1 + # save the excess data to _rbuf + self._rbuf.write(data[nl:]) + if buf_len: + buf.write(data[:nl]) + break + else: + # Shortcut. Avoid data copy through buf when + # returning a substring of our first recv(). + return data[:nl] + n = len(data) + if n == size and not buf_len: + # Shortcut. Avoid data copy through buf when + # returning exactly all of our first recv(). + return data + if n >= left: + buf.write(data[:left]) + self._rbuf.write(data[left:]) + break + buf.write(data) + buf_len += n + # assert buf_len == buf.tell() + return buf.getvalue() + else: + def read(self, size=-1): + """Read data from the socket to buffer.""" + if size < 0: + # Read until EOF + buffers = [self._rbuf] + self._rbuf = '' + if self._rbufsize <= 1: + recv_size = self.default_bufsize + else: + recv_size = self._rbufsize + + while True: + data = self.recv(recv_size) + if not data: + break + buffers.append(data) + return ''.join(buffers) + else: + # Read until size bytes or EOF seen, whichever comes first + data = self._rbuf + buf_len = len(data) + if buf_len >= size: + self._rbuf = data[size:] + return data[:size] + buffers = [] + if data: + buffers.append(data) + self._rbuf = '' + while True: + left = size - buf_len + recv_size = max(self._rbufsize, left) + data = self.recv(recv_size) + if not data: + break + buffers.append(data) + n = len(data) + if n >= left: + self._rbuf = data[left:] + buffers[-1] = data[:left] + break + buf_len += n + return ''.join(buffers) + + def readline(self, size=-1): + """Read line from the socket to buffer.""" + data = self._rbuf + if size < 0: + # Read until \n or EOF, whichever comes first + if self._rbufsize <= 1: + # Speed up unbuffered case + assert data == '' + buffers = [] + while data != '\n': + data = self.recv(1) + if not data: + break + buffers.append(data) + return ''.join(buffers) + nl = data.find('\n') + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + return data[:nl] + buffers = [] + if data: + buffers.append(data) + self._rbuf = '' + while True: + data = self.recv(self._rbufsize) + if not data: + break + buffers.append(data) + nl = data.find('\n') + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + buffers[-1] = data[:nl] + break + return ''.join(buffers) + else: + # Read until size bytes or \n or EOF seen, whichever comes + # first + nl = data.find('\n', 0, size) + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + return data[:nl] + buf_len = len(data) + if buf_len >= size: + self._rbuf = data[size:] + return data[:size] + buffers = [] + if data: + buffers.append(data) + self._rbuf = '' + while True: + data = self.recv(self._rbufsize) + if not data: + break + buffers.append(data) + left = size - buf_len + nl = data.find('\n', 0, left) + if nl >= 0: + nl += 1 + self._rbuf = data[nl:] + buffers[-1] = data[:nl] + break + n = len(data) + if n >= left: + self._rbuf = data[left:] + buffers[-1] = data[:left] + break + buf_len += n + return ''.join(buffers) + + +MakeFile = MakeFile_PY2 if six.PY2 else MakeFile_PY3 diff --git a/resources/lib/cheroot/server.py b/resources/lib/cheroot/server.py new file mode 100644 index 0000000..4407049 --- /dev/null +++ b/resources/lib/cheroot/server.py @@ -0,0 +1,2001 @@ +""" +A high-speed, production ready, thread pooled, generic HTTP server. + +For those of you wanting to understand internals of this module, here's the +basic call flow. The server's listening thread runs a very tight loop, +sticking incoming connections onto a Queue:: + + server = HTTPServer(...) + server.start() + -> while True: + tick() + # This blocks until a request comes in: + child = socket.accept() + conn = HTTPConnection(child, ...) + server.requests.put(conn) + +Worker threads are kept in a pool and poll the Queue, popping off and then +handling each connection in turn. Each connection can consist of an arbitrary +number of requests and their responses, so we run a nested loop:: + + while True: + conn = server.requests.get() + conn.communicate() + -> while True: + req = HTTPRequest(...) + req.parse_request() + -> # Read the Request-Line, e.g. "GET /page HTTP/1.1" + req.rfile.readline() + read_headers(req.rfile, req.inheaders) + req.respond() + -> response = app(...) + try: + for chunk in response: + if chunk: + req.write(chunk) + finally: + if hasattr(response, "close"): + response.close() + if req.close_connection: + return + +For running a server you can invoke :func:`start() ` (it +will run the server forever) or use invoking :func:`prepare() +` and :func:`serve() ` like this:: + + server = HTTPServer(...) + server.prepare() + try: + threading.Thread(target=server.serve).start() + + # waiting/detecting some appropriate stop condition here + ... + + finally: + server.stop() + +And now for a trivial doctest to exercise the test suite + +>>> 'HTTPServer' in globals() +True + +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import io +import re +import email.utils +import socket +import sys +import time +import traceback as traceback_ +import logging +import platform +import xbmc + +try: + from functools import lru_cache +except ImportError: + from backports.functools_lru_cache import lru_cache + +import six +from six.moves import queue +from six.moves import urllib + +from . import errors, __version__ +from ._compat import bton, ntou +from .workers import threadpool +from .makefile import MakeFile + + +__all__ = ('HTTPRequest', 'HTTPConnection', 'HTTPServer', + 'SizeCheckWrapper', 'KnownLengthRFile', 'ChunkedRFile', + 'Gateway', 'get_ssl_adapter_class') + +""" +Special KODI case: +Android does not have support for grp and pwd +But Python has issues reporting that this is running on Android (it shows as Linux2). +We're instead using xbmc library to detect that. +""" +IS_WINDOWS = platform.system() == 'Windows' +IS_ANDROID = xbmc.getCondVisibility('system.platform.linux') and xbmc.getCondVisibility('system.platform.android') + +if not (IS_WINDOWS or IS_ANDROID): + import grp + import pwd + import struct + + +if IS_WINDOWS and hasattr(socket, 'AF_INET6'): + if not hasattr(socket, 'IPPROTO_IPV6'): + socket.IPPROTO_IPV6 = 41 + if not hasattr(socket, 'IPV6_V6ONLY'): + socket.IPV6_V6ONLY = 27 + + +if not hasattr(socket, 'SO_PEERCRED'): + """ + NOTE: the value for SO_PEERCRED can be architecture specific, in + which case the getsockopt() will hopefully fail. The arch + specific value could be derived from platform.processor() + """ + socket.SO_PEERCRED = 17 + + +LF = b'\n' +CRLF = b'\r\n' +TAB = b'\t' +SPACE = b' ' +COLON = b':' +SEMICOLON = b';' +EMPTY = b'' +ASTERISK = b'*' +FORWARD_SLASH = b'/' +QUOTED_SLASH = b'%2F' +QUOTED_SLASH_REGEX = re.compile(b'(?i)' + QUOTED_SLASH) + + +comma_separated_headers = [ + b'Accept', b'Accept-Charset', b'Accept-Encoding', + b'Accept-Language', b'Accept-Ranges', b'Allow', b'Cache-Control', + b'Connection', b'Content-Encoding', b'Content-Language', b'Expect', + b'If-Match', b'If-None-Match', b'Pragma', b'Proxy-Authenticate', b'TE', + b'Trailer', b'Transfer-Encoding', b'Upgrade', b'Vary', b'Via', b'Warning', + b'WWW-Authenticate', +] + + +if not hasattr(logging, 'statistics'): + logging.statistics = {} + + +class HeaderReader: + """Object for reading headers from an HTTP request. + + Interface and default implementation. + """ + + def __call__(self, rfile, hdict=None): + """ + Read headers from the given stream into the given header dict. + + If hdict is None, a new header dict is created. Returns the populated + header dict. + + Headers which are repeated are folded together using a comma if their + specification so dictates. + + This function raises ValueError when the read bytes violate the HTTP + spec. + You should probably return "400 Bad Request" if this happens. + """ + if hdict is None: + hdict = {} + + while True: + line = rfile.readline() + if not line: + # No more data--illegal end of headers + raise ValueError('Illegal end of headers.') + + if line == CRLF: + # Normal end of headers + break + if not line.endswith(CRLF): + raise ValueError('HTTP requires CRLF terminators') + + if line[0] in (SPACE, TAB): + # It's a continuation line. + v = line.strip() + else: + try: + k, v = line.split(COLON, 1) + except ValueError: + raise ValueError('Illegal header line.') + v = v.strip() + k = self._transform_key(k) + hname = k + + if not self._allow_header(k): + continue + + if k in comma_separated_headers: + existing = hdict.get(hname) + if existing: + v = b', '.join((existing, v)) + hdict[hname] = v + + return hdict + + def _allow_header(self, key_name): + return True + + def _transform_key(self, key_name): + # TODO: what about TE and WWW-Authenticate? + return key_name.strip().title() + + +class DropUnderscoreHeaderReader(HeaderReader): + """Custom HeaderReader to exclude any headers with underscores in them.""" + + def _allow_header(self, key_name): + orig = super(DropUnderscoreHeaderReader, self)._allow_header(key_name) + return orig and '_' not in key_name + + +class SizeCheckWrapper: + """Wraps a file-like object, raising MaxSizeExceeded if too large.""" + + def __init__(self, rfile, maxlen): + """Initialize SizeCheckWrapper instance. + + Args: + rfile (file): file of a limited size + maxlen (int): maximum length of the file being read + """ + self.rfile = rfile + self.maxlen = maxlen + self.bytes_read = 0 + + def _check_length(self): + if self.maxlen and self.bytes_read > self.maxlen: + raise errors.MaxSizeExceeded() + + def read(self, size=None): + """Read a chunk from rfile buffer and return it. + + Args: + size (int): amount of data to read + + Returns: + bytes: Chunk from rfile, limited by size if specified. + + """ + data = self.rfile.read(size) + self.bytes_read += len(data) + self._check_length() + return data + + def readline(self, size=None): + """Read a single line from rfile buffer and return it. + + Args: + size (int): minimum amount of data to read + + Returns: + bytes: One line from rfile. + + """ + if size is not None: + data = self.rfile.readline(size) + self.bytes_read += len(data) + self._check_length() + return data + + # User didn't specify a size ... + # We read the line in chunks to make sure it's not a 100MB line ! + res = [] + while True: + data = self.rfile.readline(256) + self.bytes_read += len(data) + self._check_length() + res.append(data) + # See https://github.com/cherrypy/cherrypy/issues/421 + if len(data) < 256 or data[-1:] == LF: + return EMPTY.join(res) + + def readlines(self, sizehint=0): + """Read all lines from rfile buffer and return them. + + Args: + sizehint (int): hint of minimum amount of data to read + + Returns: + list[bytes]: Lines of bytes read from rfile. + + """ + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline(sizehint) + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + return lines + + def close(self): + """Release resources allocated for rfile.""" + self.rfile.close() + + def __iter__(self): + """Return file iterator.""" + return self + + def __next__(self): + """Generate next file chunk.""" + data = next(self.rfile) + self.bytes_read += len(data) + self._check_length() + return data + + next = __next__ + + +class KnownLengthRFile: + """Wraps a file-like object, returning an empty string when exhausted.""" + + def __init__(self, rfile, content_length): + """Initialize KnownLengthRFile instance. + + Args: + rfile (file): file of a known size + content_length (int): length of the file being read + + """ + self.rfile = rfile + self.remaining = content_length + + def read(self, size=None): + """Read a chunk from rfile buffer and return it. + + Args: + size (int): amount of data to read + + Returns: + bytes: Chunk from rfile, limited by size if specified. + + """ + if self.remaining == 0: + return b'' + if size is None: + size = self.remaining + else: + size = min(size, self.remaining) + + data = self.rfile.read(size) + self.remaining -= len(data) + return data + + def readline(self, size=None): + """Read a single line from rfile buffer and return it. + + Args: + size (int): minimum amount of data to read + + Returns: + bytes: One line from rfile. + + """ + if self.remaining == 0: + return b'' + if size is None: + size = self.remaining + else: + size = min(size, self.remaining) + + data = self.rfile.readline(size) + self.remaining -= len(data) + return data + + def readlines(self, sizehint=0): + """Read all lines from rfile buffer and return them. + + Args: + sizehint (int): hint of minimum amount of data to read + + Returns: + list[bytes]: Lines of bytes read from rfile. + + """ + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline(sizehint) + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + return lines + + def close(self): + """Release resources allocated for rfile.""" + self.rfile.close() + + def __iter__(self): + """Return file iterator.""" + return self + + def __next__(self): + """Generate next file chunk.""" + data = next(self.rfile) + self.remaining -= len(data) + return data + + next = __next__ + + +class ChunkedRFile: + """Wraps a file-like object, returning an empty string when exhausted. + + This class is intended to provide a conforming wsgi.input value for + request entities that have been encoded with the 'chunked' transfer + encoding. + """ + + def __init__(self, rfile, maxlen, bufsize=8192): + """Initialize ChunkedRFile instance. + + Args: + rfile (file): file encoded with the 'chunked' transfer encoding + maxlen (int): maximum length of the file being read + bufsize (int): size of the buffer used to read the file + """ + self.rfile = rfile + self.maxlen = maxlen + self.bytes_read = 0 + self.buffer = EMPTY + self.bufsize = bufsize + self.closed = False + + def _fetch(self): + if self.closed: + return + + line = self.rfile.readline() + self.bytes_read += len(line) + + if self.maxlen and self.bytes_read > self.maxlen: + raise errors.MaxSizeExceeded( + 'Request Entity Too Large', self.maxlen) + + line = line.strip().split(SEMICOLON, 1) + + try: + chunk_size = line.pop(0) + chunk_size = int(chunk_size, 16) + except ValueError: + raise ValueError('Bad chunked transfer size: ' + repr(chunk_size)) + + if chunk_size <= 0: + self.closed = True + return + +# if line: chunk_extension = line[0] + + if self.maxlen and self.bytes_read + chunk_size > self.maxlen: + raise IOError('Request Entity Too Large') + + chunk = self.rfile.read(chunk_size) + self.bytes_read += len(chunk) + self.buffer += chunk + + crlf = self.rfile.read(2) + if crlf != CRLF: + raise ValueError( + "Bad chunked transfer coding (expected '\\r\\n', " + 'got ' + repr(crlf) + ')') + + def read(self, size=None): + """Read a chunk from rfile buffer and return it. + + Args: + size (int): amount of data to read + + Returns: + bytes: Chunk from rfile, limited by size if specified. + + """ + data = EMPTY + + if size == 0: + return data + + while True: + if size and len(data) >= size: + return data + + if not self.buffer: + self._fetch() + if not self.buffer: + # EOF + return data + + if size: + remaining = size - len(data) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + data += self.buffer + self.buffer = EMPTY + + def readline(self, size=None): + """Read a single line from rfile buffer and return it. + + Args: + size (int): minimum amount of data to read + + Returns: + bytes: One line from rfile. + + """ + data = EMPTY + + if size == 0: + return data + + while True: + if size and len(data) >= size: + return data + + if not self.buffer: + self._fetch() + if not self.buffer: + # EOF + return data + + newline_pos = self.buffer.find(LF) + if size: + if newline_pos == -1: + remaining = size - len(data) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + remaining = min(size - len(data), newline_pos) + data += self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + else: + if newline_pos == -1: + data += self.buffer + self.buffer = EMPTY + else: + data += self.buffer[:newline_pos] + self.buffer = self.buffer[newline_pos:] + + def readlines(self, sizehint=0): + """Read all lines from rfile buffer and return them. + + Args: + sizehint (int): hint of minimum amount of data to read + + Returns: + list[bytes]: Lines of bytes read from rfile. + + """ + # Shamelessly stolen from StringIO + total = 0 + lines = [] + line = self.readline(sizehint) + while line: + lines.append(line) + total += len(line) + if 0 < sizehint <= total: + break + line = self.readline(sizehint) + return lines + + def read_trailer_lines(self): + """Read HTTP headers and yield them. + + Returns: + Generator: yields CRLF separated lines. + + """ + if not self.closed: + raise ValueError( + 'Cannot read trailers until the request body has been read.') + + while True: + line = self.rfile.readline() + if not line: + # No more data--illegal end of headers + raise ValueError('Illegal end of headers.') + + self.bytes_read += len(line) + if self.maxlen and self.bytes_read > self.maxlen: + raise IOError('Request Entity Too Large') + + if line == CRLF: + # Normal end of headers + break + if not line.endswith(CRLF): + raise ValueError('HTTP requires CRLF terminators') + + yield line + + def close(self): + """Release resources allocated for rfile.""" + self.rfile.close() + + +class HTTPRequest: + """An HTTP Request (and response). + + A single HTTP connection may consist of multiple request/response pairs. + """ + + server = None + """The HTTPServer object which is receiving this request.""" + + conn = None + """The HTTPConnection object on which this request connected.""" + + inheaders = {} + """A dict of request headers.""" + + outheaders = [] + """A list of header tuples to write in the response.""" + + ready = False + """When True, the request has been parsed and is ready to begin generating + the response. When False, signals the calling Connection that the response + should not be generated and the connection should close.""" + + close_connection = False + """Signals the calling Connection that the request should close. This does + not imply an error! The client and/or server may each request that the + connection be closed.""" + + chunked_write = False + """If True, output will be encoded with the "chunked" transfer-coding. + + This value is set automatically inside send_headers.""" + + header_reader = HeaderReader() + """ + A HeaderReader instance or compatible reader. + """ + + def __init__(self, server, conn, proxy_mode=False, strict_mode=True): + """Initialize HTTP request container instance. + + Args: + server (HTTPServer): web server object receiving this request + conn (HTTPConnection): HTTP connection object for this request + proxy_mode (bool): whether this HTTPServer should behave as a PROXY + server for certain requests + strict_mode (bool): whether we should return a 400 Bad Request when + we encounter a request that a HTTP compliant client should not be + making + """ + self.server = server + self.conn = conn + + self.ready = False + self.started_request = False + self.scheme = b'http' + if self.server.ssl_adapter is not None: + self.scheme = b'https' + # Use the lowest-common protocol in case read_request_line errors. + self.response_protocol = 'HTTP/1.0' + self.inheaders = {} + + self.status = '' + self.outheaders = [] + self.sent_headers = False + self.close_connection = self.__class__.close_connection + self.chunked_read = False + self.chunked_write = self.__class__.chunked_write + self.proxy_mode = proxy_mode + self.strict_mode = strict_mode + + def parse_request(self): + """Parse the next HTTP request start-line and message-headers.""" + self.rfile = SizeCheckWrapper(self.conn.rfile, + self.server.max_request_header_size) + try: + success = self.read_request_line() + except errors.MaxSizeExceeded: + self.simple_response( + '414 Request-URI Too Long', + 'The Request-URI sent with the request exceeds the maximum ' + 'allowed bytes.') + return + else: + if not success: + return + + try: + success = self.read_request_headers() + except errors.MaxSizeExceeded: + self.simple_response( + '413 Request Entity Too Large', + 'The headers sent with the request exceed the maximum ' + 'allowed bytes.') + return + else: + if not success: + return + + self.ready = True + + def read_request_line(self): + """Read and parse first line of the HTTP request. + + Returns: + bool: True if the request line is valid or False if it's malformed. + + """ + # HTTP/1.1 connections are persistent by default. If a client + # requests a page, then idles (leaves the connection open), + # then rfile.readline() will raise socket.error("timed out"). + # Note that it does this based on the value given to settimeout(), + # and doesn't need the client to request or acknowledge the close + # (although your TCP stack might suffer for it: cf Apache's history + # with FIN_WAIT_2). + request_line = self.rfile.readline() + + # Set started_request to True so communicate() knows to send 408 + # from here on out. + self.started_request = True + if not request_line: + return False + + if request_line == CRLF: + # RFC 2616 sec 4.1: "...if the server is reading the protocol + # stream at the beginning of a message and receives a CRLF + # first, it should ignore the CRLF." + # But only ignore one leading line! else we enable a DoS. + request_line = self.rfile.readline() + if not request_line: + return False + + if not request_line.endswith(CRLF): + self.simple_response( + '400 Bad Request', 'HTTP requires CRLF terminators') + return False + + try: + method, uri, req_protocol = request_line.strip().split(SPACE, 2) + if not req_protocol.startswith(b'HTTP/'): + self.simple_response( + '400 Bad Request', 'Malformed Request-Line: bad protocol' + ) + return False + rp = req_protocol[5:].split(b'.', 1) + rp = tuple(map(int, rp)) # Minor.Major must be threat as integers + if rp > (1, 1): + self.simple_response( + '505 HTTP Version Not Supported', 'Cannot fulfill request' + ) + return False + except (ValueError, IndexError): + self.simple_response('400 Bad Request', 'Malformed Request-Line') + return False + + self.uri = uri + self.method = method.upper() + + if self.strict_mode and method != self.method: + resp = ( + 'Malformed method name: According to RFC 2616 ' + '(section 5.1.1) and its successors ' + 'RFC 7230 (section 3.1.1) and RFC 7231 (section 4.1) ' + 'method names are case-sensitive and uppercase.' + ) + self.simple_response('400 Bad Request', resp) + return False + + try: + if six.PY2: # FIXME: Figure out better way to do this + # Ref: https://stackoverflow.com/a/196392/595220 (like this?) + """This is a dummy check for unicode in URI.""" + ntou(bton(uri, 'ascii'), 'ascii') + scheme, authority, path, qs, fragment = urllib.parse.urlsplit(uri) + except UnicodeError: + self.simple_response('400 Bad Request', 'Malformed Request-URI') + return False + + if self.method == b'OPTIONS': + # TODO: cover this branch with tests + path = (uri + # https://tools.ietf.org/html/rfc7230#section-5.3.4 + if self.proxy_mode or uri == ASTERISK + else path) + elif self.method == b'CONNECT': + # TODO: cover this branch with tests + if not self.proxy_mode: + self.simple_response('405 Method Not Allowed') + return False + + # `urlsplit()` above parses "example.com:3128" as path part of URI. + # this is a workaround, which makes it detect netloc correctly + uri_split = urllib.parse.urlsplit(b'//' + uri) + _scheme, _authority, _path, _qs, _fragment = uri_split + _port = EMPTY + try: + _port = uri_split.port + except ValueError: + pass + + # FIXME: use third-party validation to make checks against RFC + # the validation doesn't take into account, that urllib parses + # invalid URIs without raising errors + # https://tools.ietf.org/html/rfc7230#section-5.3.3 + invalid_path = ( + _authority != uri + or not _port + or any((_scheme, _path, _qs, _fragment)) + ) + if invalid_path: + self.simple_response('400 Bad Request', + 'Invalid path in Request-URI: request-' + 'target must match authority-form.') + return False + + authority = path = _authority + scheme = qs = fragment = EMPTY + else: + uri_is_absolute_form = (scheme or authority) + + disallowed_absolute = ( + self.strict_mode + and not self.proxy_mode + and uri_is_absolute_form + ) + if disallowed_absolute: + # https://tools.ietf.org/html/rfc7230#section-5.3.2 + # (absolute form) + """Absolute URI is only allowed within proxies.""" + self.simple_response( + '400 Bad Request', + 'Absolute URI not allowed if server is not a proxy.', + ) + return False + + invalid_path = ( + self.strict_mode + and not uri.startswith(FORWARD_SLASH) + and not uri_is_absolute_form + ) + if invalid_path: + # https://tools.ietf.org/html/rfc7230#section-5.3.1 + # (origin_form) and + """Path should start with a forward slash.""" + resp = ( + 'Invalid path in Request-URI: request-target must contain ' + 'origin-form which starts with absolute-path (URI ' + 'starting with a slash "/").' + ) + self.simple_response('400 Bad Request', resp) + return False + + if fragment: + self.simple_response('400 Bad Request', + 'Illegal #fragment in Request-URI.') + return False + + if path is None: + # FIXME: It looks like this case cannot happen + self.simple_response('400 Bad Request', + 'Invalid path in Request-URI.') + return False + + # Unquote the path+params (e.g. "/this%20path" -> "/this path"). + # https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 + # + # But note that "...a URI must be separated into its components + # before the escaped characters within those components can be + # safely decoded." https://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2 + # Therefore, "/this%2Fpath" becomes "/this%2Fpath", not + # "/this/path". + try: + # TODO: Figure out whether exception can really happen here. + # It looks like it's caught on urlsplit() call above. + atoms = [ + urllib.parse.unquote_to_bytes(x) + for x in QUOTED_SLASH_REGEX.split(path) + ] + except ValueError as ex: + self.simple_response('400 Bad Request', ex.args[0]) + return False + path = QUOTED_SLASH.join(atoms) + + if not path.startswith(FORWARD_SLASH): + path = FORWARD_SLASH + path + + if scheme is not EMPTY: + self.scheme = scheme + self.authority = authority + self.path = path + + # Note that, like wsgiref and most other HTTP servers, + # we "% HEX HEX"-unquote the path but not the query string. + self.qs = qs + + # Compare request and server HTTP protocol versions, in case our + # server does not support the requested protocol. Limit our output + # to min(req, server). We want the following output: + # request server actual written supported response + # protocol protocol response protocol feature set + # a 1.0 1.0 1.0 1.0 + # b 1.0 1.1 1.1 1.0 + # c 1.1 1.0 1.0 1.0 + # d 1.1 1.1 1.1 1.1 + # Notice that, in (b), the response will be "HTTP/1.1" even though + # the client only understands 1.0. RFC 2616 10.5.6 says we should + # only return 505 if the _major_ version is different. + sp = int(self.server.protocol[5]), int(self.server.protocol[7]) + + if sp[0] != rp[0]: + self.simple_response('505 HTTP Version Not Supported') + return False + + self.request_protocol = req_protocol + self.response_protocol = 'HTTP/%s.%s' % min(rp, sp) + + return True + + def read_request_headers(self): + """Read self.rfile into self.inheaders. Return success.""" + # then all the http headers + try: + self.header_reader(self.rfile, self.inheaders) + except ValueError as ex: + self.simple_response('400 Bad Request', ex.args[0]) + return False + + mrbs = self.server.max_request_body_size + + try: + cl = int(self.inheaders.get(b'Content-Length', 0)) + except ValueError: + self.simple_response( + '400 Bad Request', + 'Malformed Content-Length Header.') + return False + + if mrbs and cl > mrbs: + self.simple_response( + '413 Request Entity Too Large', + 'The entity sent with the request exceeds the maximum ' + 'allowed bytes.') + return False + + # Persistent connection support + if self.response_protocol == 'HTTP/1.1': + # Both server and client are HTTP/1.1 + if self.inheaders.get(b'Connection', b'') == b'close': + self.close_connection = True + else: + # Either the server or client (or both) are HTTP/1.0 + if self.inheaders.get(b'Connection', b'') != b'Keep-Alive': + self.close_connection = True + + # Transfer-Encoding support + te = None + if self.response_protocol == 'HTTP/1.1': + te = self.inheaders.get(b'Transfer-Encoding') + if te: + te = [x.strip().lower() for x in te.split(b',') if x.strip()] + + self.chunked_read = False + + if te: + for enc in te: + if enc == b'chunked': + self.chunked_read = True + else: + # Note that, even if we see "chunked", we must reject + # if there is an extension we don't recognize. + self.simple_response('501 Unimplemented') + self.close_connection = True + return False + + # From PEP 333: + # "Servers and gateways that implement HTTP 1.1 must provide + # transparent support for HTTP 1.1's "expect/continue" mechanism. + # This may be done in any of several ways: + # 1. Respond to requests containing an Expect: 100-continue request + # with an immediate "100 Continue" response, and proceed normally. + # 2. Proceed with the request normally, but provide the application + # with a wsgi.input stream that will send the "100 Continue" + # response if/when the application first attempts to read from + # the input stream. The read request must then remain blocked + # until the client responds. + # 3. Wait until the client decides that the server does not support + # expect/continue, and sends the request body on its own. + # (This is suboptimal, and is not recommended.) + # + # We used to do 3, but are now doing 1. Maybe we'll do 2 someday, + # but it seems like it would be a big slowdown for such a rare case. + if self.inheaders.get(b'Expect', b'') == b'100-continue': + # Don't use simple_response here, because it emits headers + # we don't want. See + # https://github.com/cherrypy/cherrypy/issues/951 + msg = self.server.protocol.encode('ascii') + msg += b' 100 Continue\r\n\r\n' + try: + self.conn.wfile.write(msg) + except socket.error as ex: + if ex.args[0] not in errors.socket_errors_to_ignore: + raise + return True + + def respond(self): + """Call the gateway and write its iterable output.""" + mrbs = self.server.max_request_body_size + if self.chunked_read: + self.rfile = ChunkedRFile(self.conn.rfile, mrbs) + else: + cl = int(self.inheaders.get(b'Content-Length', 0)) + if mrbs and mrbs < cl: + if not self.sent_headers: + self.simple_response( + '413 Request Entity Too Large', + 'The entity sent with the request exceeds the ' + 'maximum allowed bytes.') + return + self.rfile = KnownLengthRFile(self.conn.rfile, cl) + + self.server.gateway(self).respond() + self.ready and self.ensure_headers_sent() + + if self.chunked_write: + self.conn.wfile.write(b'0\r\n\r\n') + + def simple_response(self, status, msg=''): + """Write a simple response back to the client.""" + status = str(status) + proto_status = '%s %s\r\n' % (self.server.protocol, status) + content_length = 'Content-Length: %s\r\n' % len(msg) + content_type = 'Content-Type: text/plain\r\n' + buf = [ + proto_status.encode('ISO-8859-1'), + content_length.encode('ISO-8859-1'), + content_type.encode('ISO-8859-1'), + ] + + if status[:3] in ('413', '414'): + # Request Entity Too Large / Request-URI Too Long + self.close_connection = True + if self.response_protocol == 'HTTP/1.1': + # This will not be true for 414, since read_request_line + # usually raises 414 before reading the whole line, and we + # therefore cannot know the proper response_protocol. + buf.append(b'Connection: close\r\n') + else: + # HTTP/1.0 had no 413/414 status nor Connection header. + # Emit 400 instead and trust the message body is enough. + status = '400 Bad Request' + + buf.append(CRLF) + if msg: + if isinstance(msg, six.text_type): + msg = msg.encode('ISO-8859-1') + buf.append(msg) + + try: + self.conn.wfile.write(EMPTY.join(buf)) + except socket.error as ex: + if ex.args[0] not in errors.socket_errors_to_ignore: + raise + + def ensure_headers_sent(self): + """Ensure headers are sent to the client if not already sent.""" + if not self.sent_headers: + self.sent_headers = True + self.send_headers() + + def write(self, chunk): + """Write unbuffered data to the client.""" + if self.chunked_write and chunk: + chunk_size_hex = hex(len(chunk))[2:].encode('ascii') + buf = [chunk_size_hex, CRLF, chunk, CRLF] + self.conn.wfile.write(EMPTY.join(buf)) + else: + self.conn.wfile.write(chunk) + + def send_headers(self): + """Assert, process, and send the HTTP response message-headers. + + You must set self.status, and self.outheaders before calling this. + """ + hkeys = [key.lower() for key, value in self.outheaders] + status = int(self.status[:3]) + + if status == 413: + # Request Entity Too Large. Close conn to avoid garbage. + self.close_connection = True + elif b'content-length' not in hkeys: + # "All 1xx (informational), 204 (no content), + # and 304 (not modified) responses MUST NOT + # include a message-body." So no point chunking. + if status < 200 or status in (204, 205, 304): + pass + else: + needs_chunked = ( + self.response_protocol == 'HTTP/1.1' + and self.method != b'HEAD' + ) + if needs_chunked: + # Use the chunked transfer-coding + self.chunked_write = True + self.outheaders.append((b'Transfer-Encoding', b'chunked')) + else: + # Closing the conn is the only way to determine len. + self.close_connection = True + + if b'connection' not in hkeys: + if self.response_protocol == 'HTTP/1.1': + # Both server and client are HTTP/1.1 or better + if self.close_connection: + self.outheaders.append((b'Connection', b'close')) + else: + # Server and/or client are HTTP/1.0 + if not self.close_connection: + self.outheaders.append((b'Connection', b'Keep-Alive')) + + if (not self.close_connection) and (not self.chunked_read): + # Read any remaining request body data on the socket. + # "If an origin server receives a request that does not include an + # Expect request-header field with the "100-continue" expectation, + # the request includes a request body, and the server responds + # with a final status code before reading the entire request body + # from the transport connection, then the server SHOULD NOT close + # the transport connection until it has read the entire request, + # or until the client closes the connection. Otherwise, the client + # might not reliably receive the response message. However, this + # requirement is not be construed as preventing a server from + # defending itself against denial-of-service attacks, or from + # badly broken client implementations." + remaining = getattr(self.rfile, 'remaining', 0) + if remaining > 0: + self.rfile.read(remaining) + + if b'date' not in hkeys: + self.outheaders.append(( + b'Date', + email.utils.formatdate(usegmt=True).encode('ISO-8859-1'), + )) + + if b'server' not in hkeys: + self.outheaders.append(( + b'Server', + self.server.server_name.encode('ISO-8859-1'), + )) + + proto = self.server.protocol.encode('ascii') + buf = [proto + SPACE + self.status + CRLF] + for k, v in self.outheaders: + buf.append(k + COLON + SPACE + v + CRLF) + buf.append(CRLF) + self.conn.wfile.write(EMPTY.join(buf)) + + +class HTTPConnection: + """An HTTP connection (active socket).""" + + remote_addr = None + remote_port = None + ssl_env = None + rbufsize = io.DEFAULT_BUFFER_SIZE + wbufsize = io.DEFAULT_BUFFER_SIZE + RequestHandlerClass = HTTPRequest + peercreds_enabled = False + peercreds_resolve_enabled = False + + def __init__(self, server, sock, makefile=MakeFile): + """Initialize HTTPConnection instance. + + Args: + server (HTTPServer): web server object receiving this request + socket (socket._socketobject): the raw socket object (usually + TCP) for this connection + makefile (file): a fileobject class for reading from the socket + """ + self.server = server + self.socket = sock + self.rfile = makefile(sock, 'rb', self.rbufsize) + self.wfile = makefile(sock, 'wb', self.wbufsize) + self.requests_seen = 0 + + self.peercreds_enabled = self.server.peercreds_enabled + self.peercreds_resolve_enabled = self.server.peercreds_resolve_enabled + + # LRU cached methods: + # Ref: https://stackoverflow.com/a/14946506/595220 + self.resolve_peer_creds = ( + lru_cache(maxsize=1)(self.resolve_peer_creds) + ) + self.get_peer_creds = ( + lru_cache(maxsize=1)(self.get_peer_creds) + ) + + def communicate(self): + """Read each request and respond appropriately.""" + request_seen = False + try: + while True: + # (re)set req to None so that if something goes wrong in + # the RequestHandlerClass constructor, the error doesn't + # get written to the previous request. + req = None + req = self.RequestHandlerClass(self.server, self) + + # This order of operations should guarantee correct pipelining. + req.parse_request() + if self.server.stats['Enabled']: + self.requests_seen += 1 + if not req.ready: + # Something went wrong in the parsing (and the server has + # probably already made a simple_response). Return and + # let the conn close. + return + + request_seen = True + req.respond() + if req.close_connection: + return + except socket.error as ex: + errnum = ex.args[0] + # sadly SSL sockets return a different (longer) time out string + timeout_errs = 'timed out', 'The read operation timed out' + if errnum in timeout_errs: + # Don't error if we're between requests; only error + # if 1) no request has been started at all, or 2) we're + # in the middle of a request. + # See https://github.com/cherrypy/cherrypy/issues/853 + if (not request_seen) or (req and req.started_request): + self._conditional_error(req, '408 Request Timeout') + elif errnum not in errors.socket_errors_to_ignore: + self.server.error_log('socket.error %s' % repr(errnum), + level=logging.WARNING, traceback=True) + self._conditional_error(req, '500 Internal Server Error') + except (KeyboardInterrupt, SystemExit): + raise + except errors.FatalSSLAlert: + pass + except errors.NoSSLError: + self._handle_no_ssl(req) + except Exception as ex: + self.server.error_log( + repr(ex), level=logging.ERROR, traceback=True) + self._conditional_error(req, '500 Internal Server Error') + + linger = False + + def _handle_no_ssl(self, req): + if not req or req.sent_headers: + return + # Unwrap wfile + self.wfile = MakeFile(self.socket._sock, 'wb', self.wbufsize) + msg = ( + 'The client sent a plain HTTP request, but ' + 'this server only speaks HTTPS on this port.' + ) + req.simple_response('400 Bad Request', msg) + self.linger = True + + def _conditional_error(self, req, response): + """Respond with an error. + + Don't bother writing if a response + has already started being written. + """ + if not req or req.sent_headers: + return + + try: + req.simple_response(response) + except errors.FatalSSLAlert: + pass + except errors.NoSSLError: + self._handle_no_ssl(req) + + def close(self): + """Close the socket underlying this connection.""" + self.rfile.close() + + if not self.linger: + self._close_kernel_socket() + self.socket.close() + else: + # On the other hand, sometimes we want to hang around for a bit + # to make sure the client has a chance to read our entire + # response. Skipping the close() calls here delays the FIN + # packet until the socket object is garbage-collected later. + # Someday, perhaps, we'll do the full lingering_close that + # Apache does, but not today. + pass + + def get_peer_creds(self): # LRU cached on per-instance basis, see __init__ + """Return the PID/UID/GID tuple of the peer socket for UNIX sockets. + + This function uses SO_PEERCRED to query the UNIX PID, UID, GID + of the peer, which is only available if the bind address is + a UNIX domain socket. + + Raises: + NotImplementedError: in case of unsupported socket type + RuntimeError: in case of SO_PEERCRED lookup unsupported or disabled + + """ + PEERCRED_STRUCT_DEF = '3i' + + if IS_WINDOWS or self.socket.family != socket.AF_UNIX: + raise NotImplementedError( + 'SO_PEERCRED is only supported in Linux kernel and WSL' + ) + elif not self.peercreds_enabled: + raise RuntimeError( + 'Peer creds lookup is disabled within this server' + ) + + try: + peer_creds = self.socket.getsockopt( + socket.SOL_SOCKET, socket.SO_PEERCRED, + struct.calcsize(PEERCRED_STRUCT_DEF) + ) + except socket.error as socket_err: + """Non-Linux kernels don't support SO_PEERCRED. + + Refs: + http://welz.org.za/notes/on-peer-cred.html + https://github.com/daveti/tcpSockHack + msdn.microsoft.com/en-us/commandline/wsl/release_notes#build-15025 + """ + six.raise_from( # 3.6+: raise RuntimeError from socket_err + RuntimeError, + socket_err, + ) + else: + pid, uid, gid = struct.unpack(PEERCRED_STRUCT_DEF, peer_creds) + return pid, uid, gid + + @property + def peer_pid(self): + """Return the id of the connected peer process.""" + pid, _, _ = self.get_peer_creds() + return pid + + @property + def peer_uid(self): + """Return the user id of the connected peer process.""" + _, uid, _ = self.get_peer_creds() + return uid + + @property + def peer_gid(self): + """Return the group id of the connected peer process.""" + _, _, gid = self.get_peer_creds() + return gid + + def resolve_peer_creds(self): # LRU cached on per-instance basis + """Return the username and group tuple of the peercreds if available. + + Raises: + NotImplementedError: in case of unsupported OS + RuntimeError: in case of UID/GID lookup unsupported or disabled + + """ + if (IS_WINDOWS or IS_ANDROID): + raise NotImplementedError( + 'UID/GID lookup can only be done under UNIX-like OS' + ) + elif not self.peercreds_resolve_enabled: + raise RuntimeError( + 'UID/GID lookup is disabled within this server' + ) + + user = pwd.getpwuid(self.peer_uid).pw_name # [0] + group = grp.getgrgid(self.peer_gid).gr_name # [0] + + return user, group + + @property + def peer_user(self): + """Return the username of the connected peer process.""" + user, _ = self.resolve_peer_creds() + return user + + @property + def peer_group(self): + """Return the group of the connected peer process.""" + _, group = self.resolve_peer_creds() + return group + + def _close_kernel_socket(self): + """Close kernel socket in outdated Python versions. + + On old Python versions, + Python's socket module does NOT call close on the kernel + socket when you call socket.close(). We do so manually here + because we want this server to send a FIN TCP segment + immediately. Note this must be called *before* calling + socket.close(), because the latter drops its reference to + the kernel socket. + """ + if six.PY2 and hasattr(self.socket, '_sock'): + self.socket._sock.close() + + +def prevent_socket_inheritance(sock): + """Stub inheritance prevention. + + Dummy function, since neither fcntl nor ctypes are available. + """ + pass + + +class HTTPServer: + """An HTTP server.""" + + _bind_addr = '127.0.0.1' + _interrupt = None + + gateway = None + """A Gateway instance.""" + + minthreads = None + """The minimum number of worker threads to create (default 10).""" + + maxthreads = None + """The maximum number of worker threads to create. + + (default -1 = no limit)""" + + server_name = None + """The name of the server; defaults to ``self.version``.""" + + protocol = 'HTTP/1.1' + """The version string to write in the Status-Line of all HTTP responses. + + For example, "HTTP/1.1" is the default. This also limits the supported + features used in the response.""" + + request_queue_size = 5 + """The 'backlog' arg to socket.listen(); max queued connections. + + (default 5).""" + + shutdown_timeout = 5 + """The total time to wait for worker threads to cleanly exit. + + Specified in seconds.""" + + timeout = 10 + """The timeout in seconds for accepted connections (default 10).""" + + version = 'Cheroot/' + __version__ + """A version string for the HTTPServer.""" + + software = None + """The value to set for the SERVER_SOFTWARE entry in the WSGI environ. + + If None, this defaults to ``'%s Server' % self.version``. + """ + + ready = False + """Internal flag which indicating the socket is accepting connections.""" + + max_request_header_size = 0 + """The maximum size, in bytes, for request headers, or 0 for no limit.""" + + max_request_body_size = 0 + """The maximum size, in bytes, for request bodies, or 0 for no limit.""" + + nodelay = True + """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" + + ConnectionClass = HTTPConnection + """The class to use for handling HTTP connections.""" + + ssl_adapter = None + """An instance of ssl.Adapter (or a subclass). + + You must have the corresponding SSL driver library installed. + """ + + peercreds_enabled = False + """If True, peer cred lookup can be performed via UNIX domain socket.""" + + peercreds_resolve_enabled = False + """If True, username/group will be looked up in the OS from peercreds.""" + + def __init__( + self, bind_addr, gateway, + minthreads=10, maxthreads=-1, server_name=None, + peercreds_enabled=False, peercreds_resolve_enabled=False, + ): + """Initialize HTTPServer instance. + + Args: + bind_addr (tuple): network interface to listen to + gateway (Gateway): gateway for processing HTTP requests + minthreads (int): minimum number of threads for HTTP thread pool + maxthreads (int): maximum number of threads for HTTP thread pool + server_name (str): web server name to be advertised via Server + HTTP header + """ + self.bind_addr = bind_addr + self.gateway = gateway + + self.requests = threadpool.ThreadPool( + self, min=minthreads or 1, max=maxthreads) + + if not server_name: + server_name = self.version + self.server_name = server_name + self.peercreds_enabled = peercreds_enabled + self.peercreds_resolve_enabled = ( + peercreds_resolve_enabled and peercreds_enabled + ) + self.clear_stats() + + def clear_stats(self): + """Reset server stat counters..""" + self._start_time = None + self._run_time = 0 + self.stats = { + 'Enabled': False, + 'Bind Address': lambda s: repr(self.bind_addr), + 'Run time': lambda s: (not s['Enabled']) and -1 or self.runtime(), + 'Accepts': 0, + 'Accepts/sec': lambda s: s['Accepts'] / self.runtime(), + 'Queue': lambda s: getattr(self.requests, 'qsize', None), + 'Threads': lambda s: len(getattr(self.requests, '_threads', [])), + 'Threads Idle': lambda s: getattr(self.requests, 'idle', None), + 'Socket Errors': 0, + 'Requests': lambda s: (not s['Enabled']) and -1 or sum( + [w['Requests'](w) for w in s['Worker Threads'].values()], 0), + 'Bytes Read': lambda s: (not s['Enabled']) and -1 or sum( + [w['Bytes Read'](w) for w in s['Worker Threads'].values()], 0), + 'Bytes Written': lambda s: (not s['Enabled']) and -1 or sum( + [w['Bytes Written'](w) for w in s['Worker Threads'].values()], + 0), + 'Work Time': lambda s: (not s['Enabled']) and -1 or sum( + [w['Work Time'](w) for w in s['Worker Threads'].values()], 0), + 'Read Throughput': lambda s: (not s['Enabled']) and -1 or sum( + [w['Bytes Read'](w) / (w['Work Time'](w) or 1e-6) + for w in s['Worker Threads'].values()], 0), + 'Write Throughput': lambda s: (not s['Enabled']) and -1 or sum( + [w['Bytes Written'](w) / (w['Work Time'](w) or 1e-6) + for w in s['Worker Threads'].values()], 0), + 'Worker Threads': {}, + } + logging.statistics['Cheroot HTTPServer %d' % id(self)] = self.stats + + def runtime(self): + """Return server uptime.""" + if self._start_time is None: + return self._run_time + else: + return self._run_time + (time.time() - self._start_time) + + def __str__(self): + """Render Server instance representing bind address.""" + return '%s.%s(%r)' % (self.__module__, self.__class__.__name__, + self.bind_addr) + + @property + def bind_addr(self): + """Return the interface on which to listen for connections. + + For TCP sockets, a (host, port) tuple. Host values may be any IPv4 + or IPv6 address, or any valid hostname. The string 'localhost' is a + synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6). + The string '0.0.0.0' is a special IPv4 entry meaning "any active + interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for + IPv6. The empty string or None are not allowed. + + For UNIX sockets, supply the filename as a string. + + Systemd socket activation is automatic and doesn't require tempering + with this variable. + """ + return self._bind_addr + + @bind_addr.setter + def bind_addr(self, value): + """Set the interface on which to listen for connections.""" + if isinstance(value, tuple) and value[0] in ('', None): + # Despite the socket module docs, using '' does not + # allow AI_PASSIVE to work. Passing None instead + # returns '0.0.0.0' like we want. In other words: + # host AI_PASSIVE result + # '' Y 192.168.x.y + # '' N 192.168.x.y + # None Y 0.0.0.0 + # None N 127.0.0.1 + # But since you can get the same effect with an explicit + # '0.0.0.0', we deny both the empty string and None as values. + raise ValueError("Host values of '' or None are not allowed. " + "Use '0.0.0.0' (IPv4) or '::' (IPv6) instead " + 'to listen on all active interfaces.') + self._bind_addr = value + + def safe_start(self): + """Run the server forever, and stop it cleanly on exit.""" + try: + self.start() + except (KeyboardInterrupt, IOError): + # The time.sleep call might raise + # "IOError: [Errno 4] Interrupted function call" on KBInt. + self.error_log('Keyboard Interrupt: shutting down') + self.stop() + raise + except SystemExit: + self.error_log('SystemExit raised: shutting down') + self.stop() + raise + + def prepare(self): + """Prepare server to serving requests. + + It binds a socket's port, setups the socket to ``listen()`` and does + other preparing things. + """ + self._interrupt = None + + if self.software is None: + self.software = '%s Server' % self.version + + # Select the appropriate socket + self.socket = None + if os.getenv('LISTEN_PID', None): + # systemd socket activation + self.socket = socket.fromfd(3, socket.AF_INET, socket.SOCK_STREAM) + elif isinstance(self.bind_addr, six.string_types): + # AF_UNIX socket + + # So we can reuse the socket... + try: + os.unlink(self.bind_addr) + except Exception: + pass + + # So everyone can access the socket... + try: + os.chmod(self.bind_addr, 0o777) + except Exception: + pass + + info = [ + (socket.AF_UNIX, socket.SOCK_STREAM, 0, '', self.bind_addr)] + else: + # AF_INET or AF_INET6 socket + # Get the correct address family for our host (allows IPv6 + # addresses) + host, port = self.bind_addr + try: + info = socket.getaddrinfo( + host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM, 0, socket.AI_PASSIVE) + except socket.gaierror: + sock_type = socket.AF_INET + bind_addr = self.bind_addr + + if ':' in host: + sock_type = socket.AF_INET6 + bind_addr = bind_addr + (0, 0) + + info = [(sock_type, socket.SOCK_STREAM, 0, '', bind_addr)] + + if not self.socket: + msg = 'No socket could be created' + for res in info: + af, socktype, proto, canonname, sa = res + try: + self.bind(af, socktype, proto) + break + except socket.error as serr: + msg = '%s -- (%s: %s)' % (msg, sa, serr) + if self.socket: + self.socket.close() + self.socket = None + + if not self.socket: + raise socket.error(msg) + + # Timeout so KeyboardInterrupt can be caught on Win32 + self.socket.settimeout(1) + self.socket.listen(self.request_queue_size) + + # Create worker threads + self.requests.start() + + self.ready = True + self._start_time = time.time() + + def serve(self): + """Serve requests, after invoking :func:`prepare()`.""" + while self.ready: + try: + self.tick() + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + self.error_log('Error in HTTPServer.tick', level=logging.ERROR, + traceback=True) + + if self.interrupt: + while self.interrupt is True: + # Wait for self.stop() to complete. See _set_interrupt. + time.sleep(0.1) + if self.interrupt: + raise self.interrupt + + def start(self): + """Run the server forever. + + It is shortcut for invoking :func:`prepare()` then :func:`serve()`. + """ + # We don't have to trap KeyboardInterrupt or SystemExit here, + # because cherrypy.server already does so, calling self.stop() for us. + # If you're using this server with another framework, you should + # trap those exceptions in whatever code block calls start(). + self.prepare() + self.serve() + + def error_log(self, msg='', level=20, traceback=False): + """Write error message to log. + + Args: + msg (str): error message + level (int): logging level + traceback (bool): add traceback to output or not + """ + # Override this in subclasses as desired + sys.stderr.write(msg + '\n') + sys.stderr.flush() + if traceback: + tblines = traceback_.format_exc() + sys.stderr.write(tblines) + sys.stderr.flush() + + def bind(self, family, type, proto=0): + """Create (or recreate) the actual socket object.""" + self.socket = socket.socket(family, type, proto) + prevent_socket_inheritance(self.socket) + if not IS_WINDOWS: + # Windows has different semantics for SO_REUSEADDR, + # so don't set it. + # https://msdn.microsoft.com/en-us/library/ms740621(v=vs.85).aspx + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if self.nodelay and not isinstance(self.bind_addr, str): + self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + if self.ssl_adapter is not None: + self.socket = self.ssl_adapter.bind(self.socket) + + host, port = self.bind_addr[:2] + + # If listening on the IPV6 any address ('::' = IN6ADDR_ANY), + # activate dual-stack. See + # https://github.com/cherrypy/cherrypy/issues/871. + listening_ipv6 = ( + hasattr(socket, 'AF_INET6') + and family == socket.AF_INET6 + and host in ('::', '::0', '::0.0.0.0') + ) + if listening_ipv6: + try: + self.socket.setsockopt( + socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0) + except (AttributeError, socket.error): + # Apparently, the socket option is not available in + # this machine's TCP stack + pass + + self.socket.bind(self.bind_addr) + # TODO: keep requested bind_addr separate real bound_addr (port is + # different in case of ephemeral port 0) + self.bind_addr = self.socket.getsockname() + if family in ( + # Windows doesn't have socket.AF_UNIX, so not using it in check + socket.AF_INET, + socket.AF_INET6, + ): + """UNIX domain sockets are strings or bytes. + + In case of bytes with a leading null-byte it's an abstract socket. + """ + self.bind_addr = self.bind_addr[:2] + + def tick(self): + """Accept a new connection and put it on the Queue.""" + try: + s, addr = self.socket.accept() + if self.stats['Enabled']: + self.stats['Accepts'] += 1 + if not self.ready: + return + + prevent_socket_inheritance(s) + if hasattr(s, 'settimeout'): + s.settimeout(self.timeout) + + mf = MakeFile + ssl_env = {} + # if ssl cert and key are set, we try to be a secure HTTP server + if self.ssl_adapter is not None: + try: + s, ssl_env = self.ssl_adapter.wrap(s) + except errors.NoSSLError: + msg = ('The client sent a plain HTTP request, but ' + 'this server only speaks HTTPS on this port.') + buf = ['%s 400 Bad Request\r\n' % self.protocol, + 'Content-Length: %s\r\n' % len(msg), + 'Content-Type: text/plain\r\n\r\n', + msg] + + sock_to_make = s if six.PY3 else s._sock + wfile = mf(sock_to_make, 'wb', io.DEFAULT_BUFFER_SIZE) + try: + wfile.write(''.join(buf).encode('ISO-8859-1')) + except socket.error as ex: + if ex.args[0] not in errors.socket_errors_to_ignore: + raise + return + if not s: + return + mf = self.ssl_adapter.makefile + # Re-apply our timeout since we may have a new socket object + if hasattr(s, 'settimeout'): + s.settimeout(self.timeout) + + conn = self.ConnectionClass(self, s, mf) + + if not isinstance(self.bind_addr, six.string_types): + # optional values + # Until we do DNS lookups, omit REMOTE_HOST + if addr is None: # sometimes this can happen + # figure out if AF_INET or AF_INET6. + if len(s.getsockname()) == 2: + # AF_INET + addr = ('0.0.0.0', 0) + else: + # AF_INET6 + addr = ('::', 0) + conn.remote_addr = addr[0] + conn.remote_port = addr[1] + + conn.ssl_env = ssl_env + + try: + self.requests.put(conn) + except queue.Full: + # Just drop the conn. TODO: write 503 back? + conn.close() + return + except socket.timeout: + # The only reason for the timeout in start() is so we can + # notice keyboard interrupts on Win32, which don't interrupt + # accept() by default + return + except socket.error as ex: + if self.stats['Enabled']: + self.stats['Socket Errors'] += 1 + if ex.args[0] in errors.socket_error_eintr: + # I *think* this is right. EINTR should occur when a signal + # is received during the accept() call; all docs say retry + # the call, and I *think* I'm reading it right that Python + # will then go ahead and poll for and handle the signal + # elsewhere. See + # https://github.com/cherrypy/cherrypy/issues/707. + return + if ex.args[0] in errors.socket_errors_nonblocking: + # Just try again. See + # https://github.com/cherrypy/cherrypy/issues/479. + return + if ex.args[0] in errors.socket_errors_to_ignore: + # Our socket was closed. + # See https://github.com/cherrypy/cherrypy/issues/686. + return + raise + + @property + def interrupt(self): + """Flag interrupt of the server.""" + return self._interrupt + + @interrupt.setter + def interrupt(self, interrupt): + """Perform the shutdown of this server and save the exception.""" + self._interrupt = True + self.stop() + self._interrupt = interrupt + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + self.ready = False + if self._start_time is not None: + self._run_time += (time.time() - self._start_time) + self._start_time = None + + sock = getattr(self, 'socket', None) + if sock: + if not isinstance(self.bind_addr, six.string_types): + # Touch our own socket to make accept() return immediately. + try: + host, port = sock.getsockname()[:2] + except socket.error as ex: + if ex.args[0] not in errors.socket_errors_to_ignore: + # Changed to use error code and not message + # See + # https://github.com/cherrypy/cherrypy/issues/860. + raise + else: + # Note that we're explicitly NOT using AI_PASSIVE, + # here, because we want an actual IP to touch. + # localhost won't work if we've bound to a public IP, + # but it will if we bound to '0.0.0.0' (INADDR_ANY). + for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC, + socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + s = None + try: + s = socket.socket(af, socktype, proto) + # See + # https://groups.google.com/group/cherrypy-users/ + # browse_frm/thread/bbfe5eb39c904fe0 + s.settimeout(1.0) + s.connect((host, port)) + s.close() + except socket.error: + if s: + s.close() + if hasattr(sock, 'close'): + sock.close() + self.socket = None + + self.requests.stop(self.shutdown_timeout) + + +class Gateway: + """Base class to interface HTTPServer with other systems, such as WSGI.""" + + def __init__(self, req): + """Initialize Gateway instance with request. + + Args: + req (HTTPRequest): current HTTP request + """ + self.req = req + + def respond(self): + """Process the current request. Must be overridden in a subclass.""" + raise NotImplementedError + + +# These may either be ssl.Adapter subclasses or the string names +# of such classes (in which case they will be lazily loaded). +ssl_adapters = { + 'builtin': 'cheroot.ssl.builtin.BuiltinSSLAdapter', + 'pyopenssl': 'cheroot.ssl.pyopenssl.pyOpenSSLAdapter', +} + + +def get_ssl_adapter_class(name='builtin'): + """Return an SSL adapter class for the given name.""" + adapter = ssl_adapters[name.lower()] + if isinstance(adapter, six.string_types): + last_dot = adapter.rfind('.') + attr_name = adapter[last_dot + 1:] + mod_path = adapter[:last_dot] + + try: + mod = sys.modules[mod_path] + if mod is None: + raise KeyError() + except KeyError: + # The last [''] is important. + mod = __import__(mod_path, globals(), locals(), ['']) + + # Let an AttributeError propagate outward. + try: + adapter = getattr(mod, attr_name) + except AttributeError: + raise AttributeError("'%s' object has no attribute '%s'" + % (mod_path, attr_name)) + + return adapter diff --git a/resources/lib/cheroot/ssl/__init__.py b/resources/lib/cheroot/ssl/__init__.py new file mode 100644 index 0000000..ec1a0d9 --- /dev/null +++ b/resources/lib/cheroot/ssl/__init__.py @@ -0,0 +1,51 @@ +"""Implementation of the SSL adapter base interface.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from abc import ABCMeta, abstractmethod + +from six import add_metaclass + + +@add_metaclass(ABCMeta) +class Adapter: + """Base class for SSL driver library adapters. + + Required methods: + + * ``wrap(sock) -> (wrapped socket, ssl environ dict)`` + * ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) -> + socket file object`` + """ + + @abstractmethod + def __init__( + self, certificate, private_key, certificate_chain=None, + ciphers=None): + """Set up certificates, private key ciphers and reset context.""" + self.certificate = certificate + self.private_key = private_key + self.certificate_chain = certificate_chain + self.ciphers = ciphers + self.context = None + + @abstractmethod + def bind(self, sock): + """Wrap and return the given socket.""" + return sock + + @abstractmethod + def wrap(self, sock): + """Wrap and return the given socket, plus WSGI environ entries.""" + raise NotImplementedError + + @abstractmethod + def get_environ(self): + """Return WSGI environ entries to be merged into each request.""" + raise NotImplementedError + + @abstractmethod + def makefile(self, sock, mode='r', bufsize=-1): + """Return socket file object.""" + raise NotImplementedError diff --git a/resources/lib/cheroot/ssl/builtin.py b/resources/lib/cheroot/ssl/builtin.py new file mode 100644 index 0000000..a19f7ee --- /dev/null +++ b/resources/lib/cheroot/ssl/builtin.py @@ -0,0 +1,162 @@ +""" +A library for integrating Python's builtin ``ssl`` library with Cheroot. + +The ssl module must be importable for SSL functionality. + +To use this module, set ``HTTPServer.ssl_adapter`` to an instance of +``BuiltinSSLAdapter``. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +try: + import ssl +except ImportError: + ssl = None + +try: + from _pyio import DEFAULT_BUFFER_SIZE +except ImportError: + try: + from io import DEFAULT_BUFFER_SIZE + except ImportError: + DEFAULT_BUFFER_SIZE = -1 + +import six + +from . import Adapter +from .. import errors +from ..makefile import MakeFile + + +if six.PY3: + generic_socket_error = OSError +else: + import socket + generic_socket_error = socket.error + del socket + + +def _assert_ssl_exc_contains(exc, *msgs): + """Check whether SSL exception contains either of messages provided.""" + if len(msgs) < 1: + raise TypeError( + '_assert_ssl_exc_contains() requires ' + 'at least one message to be passed.' + ) + err_msg_lower = exc.args[1].lower() + return any(m.lower() in err_msg_lower for m in msgs) + + +class BuiltinSSLAdapter(Adapter): + """A wrapper for integrating Python's builtin ssl module with Cheroot.""" + + certificate = None + """The filename of the server SSL certificate.""" + + private_key = None + """The filename of the server's private key file.""" + + certificate_chain = None + """The filename of the certificate chain file.""" + + context = None + """The ssl.SSLContext that will be used to wrap sockets.""" + + ciphers = None + """The ciphers list of SSL.""" + + def __init__( + self, certificate, private_key, certificate_chain=None, + ciphers=None): + """Set up context in addition to base class properties if available.""" + if ssl is None: + raise ImportError('You must install the ssl module to use HTTPS.') + + super(BuiltinSSLAdapter, self).__init__( + certificate, private_key, certificate_chain, ciphers) + + self.context = ssl.create_default_context( + purpose=ssl.Purpose.CLIENT_AUTH, + cafile=certificate_chain + ) + self.context.load_cert_chain(certificate, private_key) + if self.ciphers is not None: + self.context.set_ciphers(ciphers) + + def bind(self, sock): + """Wrap and return the given socket.""" + return super(BuiltinSSLAdapter, self).bind(sock) + + def wrap(self, sock): + """Wrap and return the given socket, plus WSGI environ entries.""" + EMPTY_RESULT = None, {} + try: + s = self.context.wrap_socket( + sock, do_handshake_on_connect=True, server_side=True, + ) + except ssl.SSLError as ex: + if ex.errno == ssl.SSL_ERROR_EOF: + # This is almost certainly due to the cherrypy engine + # 'pinging' the socket to assert it's connectable; + # the 'ping' isn't SSL. + return EMPTY_RESULT + elif ex.errno == ssl.SSL_ERROR_SSL: + if _assert_ssl_exc_contains(ex, 'http request'): + # The client is speaking HTTP to an HTTPS server. + raise errors.NoSSLError + + # Check if it's one of the known errors + # Errors that are caught by PyOpenSSL, but thrown by + # built-in ssl + _block_errors = ( + 'unknown protocol', 'unknown ca', 'unknown_ca', + 'unknown error', + 'https proxy request', 'inappropriate fallback', + 'wrong version number', + 'no shared cipher', 'certificate unknown', + 'ccs received early', + ) + if _assert_ssl_exc_contains(ex, *_block_errors): + # Accepted error, let's pass + return EMPTY_RESULT + elif _assert_ssl_exc_contains(ex, 'handshake operation timed out'): + # This error is thrown by builtin SSL after a timeout + # when client is speaking HTTP to an HTTPS server. + # The connection can safely be dropped. + return EMPTY_RESULT + raise + except generic_socket_error as exc: + """It is unclear why exactly this happens. + + It's reproducible only under Python 2 with openssl>1.0 and stdlib + ``ssl`` wrapper, and only with CherryPy. + So it looks like some healthcheck tries to connect to this socket + during startup (from the same process). + + + Ref: https://github.com/cherrypy/cherrypy/issues/1618 + """ + if six.PY2 and exc.args == (0, 'Error'): + return EMPTY_RESULT + raise + return s, self.get_environ(s) + + # TODO: fill this out more with mod ssl env + def get_environ(self, sock): + """Create WSGI environ entries to be merged into each request.""" + cipher = sock.cipher() + ssl_environ = { + 'wsgi.url_scheme': 'https', + 'HTTPS': 'on', + 'SSL_PROTOCOL': cipher[1], + 'SSL_CIPHER': cipher[0] + # SSL_VERSION_INTERFACE string The mod_ssl program version + # SSL_VERSION_LIBRARY string The OpenSSL program version + } + return ssl_environ + + def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE): + """Return socket file object.""" + return MakeFile(sock, mode, bufsize) diff --git a/resources/lib/cheroot/ssl/pyopenssl.py b/resources/lib/cheroot/ssl/pyopenssl.py new file mode 100644 index 0000000..2185f85 --- /dev/null +++ b/resources/lib/cheroot/ssl/pyopenssl.py @@ -0,0 +1,267 @@ +""" +A library for integrating pyOpenSSL with Cheroot. + +The OpenSSL module must be importable for SSL functionality. +You can obtain it from `here `_. + +To use this module, set HTTPServer.ssl_adapter to an instance of +ssl.Adapter. There are two ways to use SSL: + +Method One +---------- + + * ``ssl_adapter.context``: an instance of SSL.Context. + +If this is not None, it is assumed to be an SSL.Context instance, +and will be passed to SSL.Connection on bind(). The developer is +responsible for forming a valid Context object. This approach is +to be preferred for more flexibility, e.g. if the cert and key are +streams instead of files, or need decryption, or SSL.SSLv3_METHOD +is desired instead of the default SSL.SSLv23_METHOD, etc. Consult +the pyOpenSSL documentation for complete options. + +Method Two (shortcut) +--------------------- + + * ``ssl_adapter.certificate``: the filename of the server SSL certificate. + * ``ssl_adapter.private_key``: the filename of the server's private key file. + +Both are None by default. If ssl_adapter.context is None, but .private_key +and .certificate are both given and valid, they will be read, and the +context will be automatically created from them. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import socket +import threading +import time + +try: + from OpenSSL import SSL + from OpenSSL import crypto +except ImportError: + SSL = None + +from . import Adapter +from .. import errors, server as cheroot_server +from ..makefile import MakeFile + + +class SSL_fileobject(MakeFile): + """SSL file object attached to a socket object.""" + + ssl_timeout = 3 + ssl_retry = .01 + + def _safe_call(self, is_reader, call, *args, **kwargs): + """Wrap the given call with SSL error-trapping. + + is_reader: if False EOF errors will be raised. If True, EOF errors + will return "" (to emulate normal sockets). + """ + start = time.time() + while True: + try: + return call(*args, **kwargs) + except SSL.WantReadError: + # Sleep and try again. This is dangerous, because it means + # the rest of the stack has no way of differentiating + # between a "new handshake" error and "client dropped". + # Note this isn't an endless loop: there's a timeout below. + time.sleep(self.ssl_retry) + except SSL.WantWriteError: + time.sleep(self.ssl_retry) + except SSL.SysCallError as e: + if is_reader and e.args == (-1, 'Unexpected EOF'): + return '' + + errnum = e.args[0] + if is_reader and errnum in errors.socket_errors_to_ignore: + return '' + raise socket.error(errnum) + except SSL.Error as e: + if is_reader and e.args == (-1, 'Unexpected EOF'): + return '' + + thirdarg = None + try: + thirdarg = e.args[0][0][2] + except IndexError: + pass + + if thirdarg == 'http request': + # The client is talking HTTP to an HTTPS server. + raise errors.NoSSLError() + + raise errors.FatalSSLAlert(*e.args) + + if time.time() - start > self.ssl_timeout: + raise socket.timeout('timed out') + + def recv(self, size): + """Receive message of a size from the socket.""" + return self._safe_call(True, super(SSL_fileobject, self).recv, size) + + def sendall(self, *args, **kwargs): + """Send whole message to the socket.""" + return self._safe_call(False, super(SSL_fileobject, self).sendall, + *args, **kwargs) + + def send(self, *args, **kwargs): + """Send some part of message to the socket.""" + return self._safe_call(False, super(SSL_fileobject, self).send, + *args, **kwargs) + + +class SSLConnection: + """A thread-safe wrapper for an SSL.Connection. + + ``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``. + """ + + def __init__(self, *args): + """Initialize SSLConnection instance.""" + self._ssl_conn = SSL.Connection(*args) + self._lock = threading.RLock() + + for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read', + 'renegotiate', 'bind', 'listen', 'connect', 'accept', + 'setblocking', 'fileno', 'close', 'get_cipher_list', + 'getpeername', 'getsockname', 'getsockopt', 'setsockopt', + 'makefile', 'get_app_data', 'set_app_data', 'state_string', + 'sock_shutdown', 'get_peer_certificate', 'want_read', + 'want_write', 'set_connect_state', 'set_accept_state', + 'connect_ex', 'sendall', 'settimeout', 'gettimeout'): + exec("""def %s(self, *args): + self._lock.acquire() + try: + return self._ssl_conn.%s(*args) + finally: + self._lock.release() +""" % (f, f)) + + def shutdown(self, *args): + """Shutdown the SSL connection. + + Ignore all incoming args since pyOpenSSL.socket.shutdown takes no args. + """ + self._lock.acquire() + try: + return self._ssl_conn.shutdown() + finally: + self._lock.release() + + +class pyOpenSSLAdapter(Adapter): + """A wrapper for integrating pyOpenSSL with Cheroot.""" + + certificate = None + """The filename of the server SSL certificate.""" + + private_key = None + """The filename of the server's private key file.""" + + certificate_chain = None + """Optional. The filename of CA's intermediate certificate bundle. + + This is needed for cheaper "chained root" SSL certificates, and should be + left as None if not required.""" + + context = None + """An instance of SSL.Context.""" + + ciphers = None + """The ciphers list of SSL.""" + + def __init__( + self, certificate, private_key, certificate_chain=None, + ciphers=None): + """Initialize OpenSSL Adapter instance.""" + if SSL is None: + raise ImportError('You must install pyOpenSSL to use HTTPS.') + + super(pyOpenSSLAdapter, self).__init__( + certificate, private_key, certificate_chain, ciphers) + + self._environ = None + + def bind(self, sock): + """Wrap and return the given socket.""" + if self.context is None: + self.context = self.get_context() + conn = SSLConnection(self.context, sock) + self._environ = self.get_environ() + return conn + + def wrap(self, sock): + """Wrap and return the given socket, plus WSGI environ entries.""" + return sock, self._environ.copy() + + def get_context(self): + """Return an SSL.Context from self attributes.""" + # See https://code.activestate.com/recipes/442473/ + c = SSL.Context(SSL.SSLv23_METHOD) + c.use_privatekey_file(self.private_key) + if self.certificate_chain: + c.load_verify_locations(self.certificate_chain) + c.use_certificate_file(self.certificate) + return c + + def get_environ(self): + """Return WSGI environ entries to be merged into each request.""" + ssl_environ = { + 'HTTPS': 'on', + # pyOpenSSL doesn't provide access to any of these AFAICT + # 'SSL_PROTOCOL': 'SSLv2', + # SSL_CIPHER string The cipher specification name + # SSL_VERSION_INTERFACE string The mod_ssl program version + # SSL_VERSION_LIBRARY string The OpenSSL program version + } + + if self.certificate: + # Server certificate attributes + cert = open(self.certificate, 'rb').read() + cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert) + ssl_environ.update({ + 'SSL_SERVER_M_VERSION': cert.get_version(), + 'SSL_SERVER_M_SERIAL': cert.get_serial_number(), + # 'SSL_SERVER_V_START': + # Validity of server's certificate (start time), + # 'SSL_SERVER_V_END': + # Validity of server's certificate (end time), + }) + + for prefix, dn in [('I', cert.get_issuer()), + ('S', cert.get_subject())]: + # X509Name objects don't seem to have a way to get the + # complete DN string. Use str() and slice it instead, + # because str(dn) == "" + dnstr = str(dn)[18:-2] + + wsgikey = 'SSL_SERVER_%s_DN' % prefix + ssl_environ[wsgikey] = dnstr + + # The DN should be of the form: /k1=v1/k2=v2, but we must allow + # for any value to contain slashes itself (in a URL). + while dnstr: + pos = dnstr.rfind('=') + dnstr, value = dnstr[:pos], dnstr[pos + 1:] + pos = dnstr.rfind('/') + dnstr, key = dnstr[:pos], dnstr[pos + 1:] + if key and value: + wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key) + ssl_environ[wsgikey] = value + + return ssl_environ + + def makefile(self, sock, mode='r', bufsize=-1): + """Return socket file object.""" + if SSL and isinstance(sock, SSL.ConnectionType): + timeout = sock.gettimeout() + f = SSL_fileobject(sock, mode, bufsize) + f.ssl_timeout = timeout + return f + else: + return cheroot_server.CP_fileobject(sock, mode, bufsize) diff --git a/resources/lib/cheroot/test/__init__.py b/resources/lib/cheroot/test/__init__.py new file mode 100644 index 0000000..e2a7b34 --- /dev/null +++ b/resources/lib/cheroot/test/__init__.py @@ -0,0 +1 @@ +"""Cheroot test suite.""" diff --git a/resources/lib/cheroot/test/conftest.py b/resources/lib/cheroot/test/conftest.py new file mode 100644 index 0000000..9f5f928 --- /dev/null +++ b/resources/lib/cheroot/test/conftest.py @@ -0,0 +1,27 @@ +"""Pytest configuration module. + +Contains fixtures, which are tightly bound to the Cheroot framework +itself, useless for end-users' app testing. +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pytest + +from ..testing import ( # noqa: F401 + native_server, wsgi_server, +) +from ..testing import get_server_client + + +@pytest.fixture # noqa: F811 +def wsgi_server_client(wsgi_server): + """Create a test client out of given WSGI server.""" + return get_server_client(wsgi_server) + + +@pytest.fixture # noqa: F811 +def native_server_client(native_server): + """Create a test client out of given HTTP server.""" + return get_server_client(native_server) diff --git a/resources/lib/cheroot/test/helper.py b/resources/lib/cheroot/test/helper.py new file mode 100644 index 0000000..38f40b2 --- /dev/null +++ b/resources/lib/cheroot/test/helper.py @@ -0,0 +1,169 @@ +"""A library of helper functions for the Cheroot test suite.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import datetime +import logging +import os +import sys +import time +import threading +import types + +from six.moves import http_client + +import six + +import cheroot.server +import cheroot.wsgi + +from cheroot.test import webtest + +log = logging.getLogger(__name__) +thisdir = os.path.abspath(os.path.dirname(__file__)) +serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem') + + +config = { + 'bind_addr': ('127.0.0.1', 54583), + 'server': 'wsgi', + 'wsgi_app': None, +} + + +class CherootWebCase(webtest.WebCase): + """Helper class for a web app test suite.""" + + script_name = '' + scheme = 'http' + + available_servers = { + 'wsgi': cheroot.wsgi.Server, + 'native': cheroot.server.HTTPServer, + } + + @classmethod + def setup_class(cls): + """Create and run one HTTP server per class.""" + conf = config.copy() + conf.update(getattr(cls, 'config', {})) + + s_class = conf.pop('server', 'wsgi') + server_factory = cls.available_servers.get(s_class) + if server_factory is None: + raise RuntimeError('Unknown server in config: %s' % conf['server']) + cls.httpserver = server_factory(**conf) + + cls.HOST, cls.PORT = cls.httpserver.bind_addr + if cls.httpserver.ssl_adapter is None: + ssl = '' + cls.scheme = 'http' + else: + ssl = ' (ssl)' + cls.HTTP_CONN = http_client.HTTPSConnection + cls.scheme = 'https' + + v = sys.version.split()[0] + log.info('Python version used to run this test script: %s' % v) + log.info('Cheroot version: %s' % cheroot.__version__) + log.info('HTTP server version: %s%s' % (cls.httpserver.protocol, ssl)) + log.info('PID: %s' % os.getpid()) + + if hasattr(cls, 'setup_server'): + # Clear the wsgi server so that + # it can be updated with the new root + cls.setup_server() + cls.start() + + @classmethod + def teardown_class(cls): + """Cleanup HTTP server.""" + if hasattr(cls, 'setup_server'): + cls.stop() + + @classmethod + def start(cls): + """Load and start the HTTP server.""" + threading.Thread(target=cls.httpserver.safe_start).start() + while not cls.httpserver.ready: + time.sleep(0.1) + + @classmethod + def stop(cls): + """Terminate HTTP server.""" + cls.httpserver.stop() + td = getattr(cls, 'teardown', None) + if td: + td() + + date_tolerance = 2 + + def assertEqualDates(self, dt1, dt2, seconds=None): + """Assert abs(dt1 - dt2) is within Y seconds.""" + if seconds is None: + seconds = self.date_tolerance + + if dt1 > dt2: + diff = dt1 - dt2 + else: + diff = dt2 - dt1 + if not diff < datetime.timedelta(seconds=seconds): + raise AssertionError('%r and %r are not within %r seconds.' % + (dt1, dt2, seconds)) + + +class Request: + """HTTP request container.""" + + def __init__(self, environ): + """Initialize HTTP request.""" + self.environ = environ + + +class Response: + """HTTP response container.""" + + def __init__(self): + """Initialize HTTP response.""" + self.status = '200 OK' + self.headers = {'Content-Type': 'text/html'} + self.body = None + + def output(self): + """Generate iterable response body object.""" + if self.body is None: + return [] + elif isinstance(self.body, six.text_type): + return [self.body.encode('iso-8859-1')] + elif isinstance(self.body, six.binary_type): + return [self.body] + else: + return [x.encode('iso-8859-1') for x in self.body] + + +class Controller: + """WSGI app for tests.""" + + def __call__(self, environ, start_response): + """WSGI request handler.""" + req, resp = Request(environ), Response() + try: + # Python 3 supports unicode attribute names + # Python 2 encodes them + handler = self.handlers[environ['PATH_INFO']] + except KeyError: + resp.status = '404 Not Found' + else: + output = handler(req, resp) + if (output is not None and + not any(resp.status.startswith(status_code) + for status_code in ('204', '304'))): + resp.body = output + try: + resp.headers.setdefault('Content-Length', str(len(output))) + except TypeError: + if not isinstance(output, types.GeneratorType): + raise + start_response(resp.status, resp.headers.items()) + return resp.output() diff --git a/resources/lib/cheroot/test/test.pem b/resources/lib/cheroot/test/test.pem new file mode 100644 index 0000000..47a4704 --- /dev/null +++ b/resources/lib/cheroot/test/test.pem @@ -0,0 +1,38 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDBKo554mzIMY+AByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZ +R9L4WtImEew05FY3Izerfm3MN3+MC0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Kn +da+O6xldVSosu8Ev3z9VZ94iC/ZgKzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQAB +AoGAWOCF0ZrWxn3XMucWq2LNwPKqlvVGwbIwX3cDmX22zmnM4Fy6arXbYh4XlyCj +9+ofqRrxIFz5k/7tFriTmZ0xag5+Jdx+Kwg0/twiP7XCNKipFogwe1Hznw8OFAoT +enKBdj2+/n2o0Bvo/tDB59m9L/538d46JGQUmJlzMyqYikECQQDyoq+8CtMNvE18 +8VgHcR/KtApxWAjj4HpaHYL637ATjThetUZkW92mgDgowyplthusxdNqhHWyv7E8 +tWNdYErZAkEAy85ShTR0M5aWmrE7o0r0SpWInAkNBH9aXQRRARFYsdBtNfRu6I0i +0lvU9wiu3eF57FMEC86yViZ5UBnQfTu7vQJAVesj/Zt7pwaCDfdMa740OsxMUlyR +MVhhGx4OLpYdPJ8qUecxGQKq13XZ7R1HGyNEY4bd2X80Smq08UFuATfC6QJAH8UB +yBHtKz2GLIcELOg6PIYizW/7v3+6rlVF60yw7sb2vzpjL40QqIn4IKoR2DSVtOkb +8FtAIX3N21aq0VrGYQJBAIPiaEc2AZ8Bq2GC4F3wOz/BxJ/izvnkiotR12QK4fh5 +yjZMhTjWCas5zwHR5PDjlD88AWGDMsZ1PicD4348xJQ= +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAy6gAwIBAgIJAI18BD7eQxlGMA0GCSqGSIb3DQEBBAUAMIGeMQswCQYD +VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU2FuIERpZWdv +MRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0MREwDwYDVQQLEwhkZXYtdGVzdDEW +MBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4GCSqGSIb3DQEJARYRcmVtaUBjaGVy +cnlweS5vcmcwHhcNMDYwOTA5MTkyMDIwWhcNMzQwMTI0MTkyMDIwWjCBnjELMAkG +A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVNhbiBEaWVn +bzEZMBcGA1UEChMQQ2hlcnJ5UHkgUHJvamVjdDERMA8GA1UECxMIZGV2LXRlc3Qx +FjAUBgNVBAMTDUNoZXJyeVB5IFRlYW0xIDAeBgkqhkiG9w0BCQEWEXJlbWlAY2hl +cnJ5cHkub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBKo554mzIMY+A +ByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZR9L4WtImEew05FY3Izerfm3MN3+M +C0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Knda+O6xldVSosu8Ev3z9VZ94iC/Zg +KzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQABo4IBBzCCAQMwHQYDVR0OBBYEFDIQ +2feb71tVZCWpU0qJ/Tw+wdtoMIHTBgNVHSMEgcswgciAFDIQ2feb71tVZCWpU0qJ +/Tw+wdtooYGkpIGhMIGeMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p +YTESMBAGA1UEBxMJU2FuIERpZWdvMRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0 +MREwDwYDVQQLEwhkZXYtdGVzdDEWMBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4G +CSqGSIb3DQEJARYRcmVtaUBjaGVycnlweS5vcmeCCQCNfAQ+3kMZRjAMBgNVHRME +BTADAQH/MA0GCSqGSIb3DQEBBAUAA4GBAL7AAQz7IePV48ZTAFHKr88ntPALsL5S +8vHCZPNMevNkLTj3DYUw2BcnENxMjm1kou2F2BkvheBPNZKIhc6z4hAml3ed1xa2 +D7w6e6OTcstdK/+KrPDDHeOP1dhMWNs2JE1bNlfF1LiXzYKSXpe88eCKjCXsCT/T +NluCaWQys3MS +-----END CERTIFICATE----- diff --git a/resources/lib/cheroot/test/test__compat.py b/resources/lib/cheroot/test/test__compat.py new file mode 100644 index 0000000..d34e5eb --- /dev/null +++ b/resources/lib/cheroot/test/test__compat.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +"""Test suite for cross-python compatibility helpers.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pytest +import six + +from cheroot._compat import ntob, ntou, bton + + +@pytest.mark.parametrize( + 'func,inp,out', + [ + (ntob, 'bar', b'bar'), + (ntou, 'bar', u'bar'), + (bton, b'bar', 'bar'), + ], +) +def test_compat_functions_positive(func, inp, out): + """Check that compat functions work with correct input.""" + assert func(inp, encoding='utf-8') == out + + +@pytest.mark.parametrize( + 'func', + [ + ntob, + ntou, + ], +) +def test_compat_functions_negative_nonnative(func): + """Check that compat functions fail loudly for incorrect input.""" + non_native_test_str = b'bar' if six.PY3 else u'bar' + with pytest.raises(TypeError): + func(non_native_test_str, encoding='utf-8') + + +@pytest.mark.skip(reason='This test does not work now') +@pytest.mark.skipif( + six.PY3, + reason='This code path only appears in Python 2 version.', +) +def test_ntou_escape(): + """Check that ntou supports escape-encoding under Python 2.""" + expected = u'' + actual = ntou('hi'.encode('ISO-8859-1'), encoding='escape') + assert actual == expected diff --git a/resources/lib/cheroot/test/test_conn.py b/resources/lib/cheroot/test/test_conn.py new file mode 100644 index 0000000..f543dd9 --- /dev/null +++ b/resources/lib/cheroot/test/test_conn.py @@ -0,0 +1,897 @@ +"""Tests for TCP connection handling, including proper and timely close.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import socket +import time + +from six.moves import range, http_client, urllib + +import six +import pytest + +from cheroot.test import helper, webtest + + +timeout = 1 +pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN' + + +class Controller(helper.Controller): + """Controller for serving WSGI apps.""" + + def hello(req, resp): + """Render Hello world.""" + return 'Hello, world!' + + def pov(req, resp): + """Render pov value.""" + return pov + + def stream(req, resp): + """Render streaming response.""" + if 'set_cl' in req.environ['QUERY_STRING']: + resp.headers['Content-Length'] = str(10) + + def content(): + for x in range(10): + yield str(x) + + return content() + + def upload(req, resp): + """Process file upload and render thank.""" + if not req.environ['REQUEST_METHOD'] == 'POST': + raise AssertionError("'POST' != request.method %r" % + req.environ['REQUEST_METHOD']) + return "thanks for '%s'" % req.environ['wsgi.input'].read() + + def custom_204(req, resp): + """Render response with status 204.""" + resp.status = '204' + return 'Code = 204' + + def custom_304(req, resp): + """Render response with status 304.""" + resp.status = '304' + return 'Code = 304' + + def err_before_read(req, resp): + """Render response with status 500.""" + resp.status = '500 Internal Server Error' + return 'ok' + + def one_megabyte_of_a(req, resp): + """Render 1MB response.""" + return ['a' * 1024] * 1024 + + def wrong_cl_buffered(req, resp): + """Render buffered response with invalid length value.""" + resp.headers['Content-Length'] = '5' + return 'I have too many bytes' + + def wrong_cl_unbuffered(req, resp): + """Render unbuffered response with invalid length value.""" + resp.headers['Content-Length'] = '5' + return ['I too', ' have too many bytes'] + + def _munge(string): + """Encode PATH_INFO correctly depending on Python version. + + WSGI 1.0 is a mess around unicode. Create endpoints + that match the PATH_INFO that it produces. + """ + if six.PY3: + return string.encode('utf-8').decode('latin-1') + return string + + handlers = { + '/hello': hello, + '/pov': pov, + '/page1': pov, + '/page2': pov, + '/page3': pov, + '/stream': stream, + '/upload': upload, + '/custom/204': custom_204, + '/custom/304': custom_304, + '/err_before_read': err_before_read, + '/one_megabyte_of_a': one_megabyte_of_a, + '/wrong_cl_buffered': wrong_cl_buffered, + '/wrong_cl_unbuffered': wrong_cl_unbuffered, + } + + +@pytest.fixture +def testing_server(wsgi_server_client): + """Attach a WSGI app to the given server and pre-configure it.""" + app = Controller() + + def _timeout(req, resp): + return str(wsgi_server.timeout) + app.handlers['/timeout'] = _timeout + wsgi_server = wsgi_server_client.server_instance + wsgi_server.wsgi_app = app + wsgi_server.max_request_body_size = 1001 + wsgi_server.timeout = timeout + wsgi_server.server_client = wsgi_server_client + return wsgi_server + + +@pytest.fixture +def test_client(testing_server): + """Get and return a test client out of the given server.""" + return testing_server.server_client + + +def header_exists(header_name, headers): + """Check that a header is present.""" + return header_name.lower() in (k.lower() for (k, _) in headers) + + +def header_has_value(header_name, header_value, headers): + """Check that a header with a given value is present.""" + return header_name.lower() in (k.lower() for (k, v) in headers + if v == header_value) + + +def test_HTTP11_persistent_connections(test_client): + """Test persistent HTTP/1.1 connections.""" + # Initialize a persistent HTTP connection + http_connection = test_client.get_connection() + http_connection.auto_open = False + http_connection.connect() + + # Make the first request and assert there's no "Connection: close". + status_line, actual_headers, actual_resp_body = test_client.get( + '/pov', http_conn=http_connection + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert not header_exists('Connection', actual_headers) + + # Make another request on the same connection. + status_line, actual_headers, actual_resp_body = test_client.get( + '/page1', http_conn=http_connection + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert not header_exists('Connection', actual_headers) + + # Test client-side close. + status_line, actual_headers, actual_resp_body = test_client.get( + '/page2', http_conn=http_connection, + headers=[('Connection', 'close')] + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert header_has_value('Connection', 'close', actual_headers) + + # Make another request on the same connection, which should error. + with pytest.raises(http_client.NotConnected): + test_client.get('/pov', http_conn=http_connection) + + +@pytest.mark.parametrize( + 'set_cl', + ( + False, # Without Content-Length + True, # With Content-Length + ) +) +def test_streaming_11(test_client, set_cl): + """Test serving of streaming responses with HTTP/1.1 protocol.""" + # Initialize a persistent HTTP connection + http_connection = test_client.get_connection() + http_connection.auto_open = False + http_connection.connect() + + # Make the first request and assert there's no "Connection: close". + status_line, actual_headers, actual_resp_body = test_client.get( + '/pov', http_conn=http_connection + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert not header_exists('Connection', actual_headers) + + # Make another, streamed request on the same connection. + if set_cl: + # When a Content-Length is provided, the content should stream + # without closing the connection. + status_line, actual_headers, actual_resp_body = test_client.get( + '/stream?set_cl=Yes', http_conn=http_connection + ) + assert header_exists('Content-Length', actual_headers) + assert not header_has_value('Connection', 'close', actual_headers) + assert not header_exists('Transfer-Encoding', actual_headers) + + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == b'0123456789' + else: + # When no Content-Length response header is provided, + # streamed output will either close the connection, or use + # chunked encoding, to determine transfer-length. + status_line, actual_headers, actual_resp_body = test_client.get( + '/stream', http_conn=http_connection + ) + assert not header_exists('Content-Length', actual_headers) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == b'0123456789' + + chunked_response = False + for k, v in actual_headers: + if k.lower() == 'transfer-encoding': + if str(v) == 'chunked': + chunked_response = True + + if chunked_response: + assert not header_has_value('Connection', 'close', actual_headers) + else: + assert header_has_value('Connection', 'close', actual_headers) + + # Make another request on the same connection, which should + # error. + with pytest.raises(http_client.NotConnected): + test_client.get('/pov', http_conn=http_connection) + + # Try HEAD. + # See https://www.bitbucket.org/cherrypy/cherrypy/issue/864. + # TODO: figure out how can this be possible on an closed connection + # (chunked_response case) + status_line, actual_headers, actual_resp_body = test_client.head( + '/stream', http_conn=http_connection + ) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == b'' + assert not header_exists('Transfer-Encoding', actual_headers) + + +@pytest.mark.parametrize( + 'set_cl', + ( + False, # Without Content-Length + True, # With Content-Length + ) +) +def test_streaming_10(test_client, set_cl): + """Test serving of streaming responses with HTTP/1.0 protocol.""" + original_server_protocol = test_client.server_instance.protocol + test_client.server_instance.protocol = 'HTTP/1.0' + + # Initialize a persistent HTTP connection + http_connection = test_client.get_connection() + http_connection.auto_open = False + http_connection.connect() + + # Make the first request and assert Keep-Alive. + status_line, actual_headers, actual_resp_body = test_client.get( + '/pov', http_conn=http_connection, + headers=[('Connection', 'Keep-Alive')], + protocol='HTTP/1.0', + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert header_has_value('Connection', 'Keep-Alive', actual_headers) + + # Make another, streamed request on the same connection. + if set_cl: + # When a Content-Length is provided, the content should + # stream without closing the connection. + status_line, actual_headers, actual_resp_body = test_client.get( + '/stream?set_cl=Yes', http_conn=http_connection, + headers=[('Connection', 'Keep-Alive')], + protocol='HTTP/1.0', + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == b'0123456789' + + assert header_exists('Content-Length', actual_headers) + assert header_has_value('Connection', 'Keep-Alive', actual_headers) + assert not header_exists('Transfer-Encoding', actual_headers) + else: + # When a Content-Length is not provided, + # the server should close the connection. + status_line, actual_headers, actual_resp_body = test_client.get( + '/stream', http_conn=http_connection, + headers=[('Connection', 'Keep-Alive')], + protocol='HTTP/1.0', + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == b'0123456789' + + assert not header_exists('Content-Length', actual_headers) + assert not header_has_value('Connection', 'Keep-Alive', actual_headers) + assert not header_exists('Transfer-Encoding', actual_headers) + + # Make another request on the same connection, which should error. + with pytest.raises(http_client.NotConnected): + test_client.get( + '/pov', http_conn=http_connection, + protocol='HTTP/1.0', + ) + + test_client.server_instance.protocol = original_server_protocol + + +@pytest.mark.parametrize( + 'http_server_protocol', + ( + 'HTTP/1.0', + 'HTTP/1.1', + ) +) +def test_keepalive(test_client, http_server_protocol): + """Test Keep-Alive enabled connections.""" + original_server_protocol = test_client.server_instance.protocol + test_client.server_instance.protocol = http_server_protocol + + http_client_protocol = 'HTTP/1.0' + + # Initialize a persistent HTTP connection + http_connection = test_client.get_connection() + http_connection.auto_open = False + http_connection.connect() + + # Test a normal HTTP/1.0 request. + status_line, actual_headers, actual_resp_body = test_client.get( + '/page2', + protocol=http_client_protocol, + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert not header_exists('Connection', actual_headers) + + # Test a keep-alive HTTP/1.0 request. + + status_line, actual_headers, actual_resp_body = test_client.get( + '/page3', headers=[('Connection', 'Keep-Alive')], + http_conn=http_connection, protocol=http_client_protocol, + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert header_has_value('Connection', 'Keep-Alive', actual_headers) + + # Remove the keep-alive header again. + status_line, actual_headers, actual_resp_body = test_client.get( + '/page3', http_conn=http_connection, + protocol=http_client_protocol, + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert not header_exists('Connection', actual_headers) + + test_client.server_instance.protocol = original_server_protocol + + +@pytest.mark.parametrize( + 'timeout_before_headers', + ( + True, + False, + ) +) +def test_HTTP11_Timeout(test_client, timeout_before_headers): + """Check timeout without sending any data. + + The server will close the conn with a 408. + """ + conn = test_client.get_connection() + conn.auto_open = False + conn.connect() + + if not timeout_before_headers: + # Connect but send half the headers only. + conn.send(b'GET /hello HTTP/1.1') + conn.send(('Host: %s' % conn.host).encode('ascii')) + # else: Connect but send nothing. + + # Wait for our socket timeout + time.sleep(timeout * 2) + + # The request should have returned 408 already. + response = conn.response_class(conn.sock, method='GET') + response.begin() + assert response.status == 408 + conn.close() + + +def test_HTTP11_Timeout_after_request(test_client): + """Check timeout after at least one request has succeeded. + + The server should close the connection without 408. + """ + fail_msg = "Writing to timed out socket didn't fail as it should have: %s" + + # Make an initial request + conn = test_client.get_connection() + conn.putrequest('GET', '/timeout?t=%s' % timeout, skip_host=True) + conn.putheader('Host', conn.host) + conn.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + assert response.status == 200 + actual_body = response.read() + expected_body = str(timeout).encode() + assert actual_body == expected_body + + # Make a second request on the same socket + conn._output(b'GET /hello HTTP/1.1') + conn._output(('Host: %s' % conn.host).encode('ascii')) + conn._send_output() + response = conn.response_class(conn.sock, method='GET') + response.begin() + assert response.status == 200 + actual_body = response.read() + expected_body = b'Hello, world!' + assert actual_body == expected_body + + # Wait for our socket timeout + time.sleep(timeout * 2) + + # Make another request on the same socket, which should error + conn._output(b'GET /hello HTTP/1.1') + conn._output(('Host: %s' % conn.host).encode('ascii')) + conn._send_output() + response = conn.response_class(conn.sock, method='GET') + try: + response.begin() + except (socket.error, http_client.BadStatusLine): + pass + except Exception as ex: + pytest.fail(fail_msg % ex) + else: + if response.status != 408: + pytest.fail(fail_msg % response.read()) + + conn.close() + + # Make another request on a new socket, which should work + conn = test_client.get_connection() + conn.putrequest('GET', '/pov', skip_host=True) + conn.putheader('Host', conn.host) + conn.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + assert response.status == 200 + actual_body = response.read() + expected_body = pov.encode() + assert actual_body == expected_body + + # Make another request on the same socket, + # but timeout on the headers + conn.send(b'GET /hello HTTP/1.1') + # Wait for our socket timeout + time.sleep(timeout * 2) + response = conn.response_class(conn.sock, method='GET') + try: + response.begin() + except (socket.error, http_client.BadStatusLine): + pass + except Exception as ex: + pytest.fail(fail_msg % ex) + else: + if response.status != 408: + pytest.fail(fail_msg % response.read()) + + conn.close() + + # Retry the request on a new connection, which should work + conn = test_client.get_connection() + conn.putrequest('GET', '/pov', skip_host=True) + conn.putheader('Host', conn.host) + conn.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + assert response.status == 200 + actual_body = response.read() + expected_body = pov.encode() + assert actual_body == expected_body + conn.close() + + +def test_HTTP11_pipelining(test_client): + """Test HTTP/1.1 pipelining. + + httplib doesn't support this directly. + """ + conn = test_client.get_connection() + + # Put request 1 + conn.putrequest('GET', '/hello', skip_host=True) + conn.putheader('Host', conn.host) + conn.endheaders() + + for trial in range(5): + # Put next request + conn._output( + ('GET /hello?%s HTTP/1.1' % trial).encode('iso-8859-1') + ) + conn._output(('Host: %s' % conn.host).encode('ascii')) + conn._send_output() + + # Retrieve previous response + response = conn.response_class(conn.sock, method='GET') + # there is a bug in python3 regarding the buffering of + # ``conn.sock``. Until that bug get's fixed we will + # monkey patch the ``reponse`` instance. + # https://bugs.python.org/issue23377 + if six.PY3: + response.fp = conn.sock.makefile('rb', 0) + response.begin() + body = response.read(13) + assert response.status == 200 + assert body == b'Hello, world!' + + # Retrieve final response + response = conn.response_class(conn.sock, method='GET') + response.begin() + body = response.read() + assert response.status == 200 + assert body == b'Hello, world!' + + conn.close() + + +def test_100_Continue(test_client): + """Test 100-continue header processing.""" + conn = test_client.get_connection() + + # Try a page without an Expect request header first. + # Note that httplib's response.begin automatically ignores + # 100 Continue responses, so we must manually check for it. + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', conn.host) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '4') + conn.endheaders() + conn.send(b"d'oh") + response = conn.response_class(conn.sock, method='POST') + version, status, reason = response._read_status() + assert status != 100 + conn.close() + + # Now try a page with an Expect header... + conn.connect() + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', conn.host) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '17') + conn.putheader('Expect', '100-continue') + conn.endheaders() + response = conn.response_class(conn.sock, method='POST') + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + assert status == 100 + while True: + line = response.fp.readline().strip() + if line: + pytest.fail( + '100 Continue should not output any headers. Got %r' % + line) + else: + break + + # ...send the body + body = b'I am a small file' + conn.send(body) + + # ...get the final response + response.begin() + status_line, actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + assert actual_status == 200 + expected_resp_body = ("thanks for '%s'" % body).encode() + assert actual_resp_body == expected_resp_body + conn.close() + + +@pytest.mark.parametrize( + 'max_request_body_size', + ( + 0, + 1001, + ) +) +def test_readall_or_close(test_client, max_request_body_size): + """Test a max_request_body_size of 0 (the default) and 1001.""" + old_max = test_client.server_instance.max_request_body_size + + test_client.server_instance.max_request_body_size = max_request_body_size + + conn = test_client.get_connection() + + # Get a POST page with an error + conn.putrequest('POST', '/err_before_read', skip_host=True) + conn.putheader('Host', conn.host) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '1000') + conn.putheader('Expect', '100-continue') + conn.endheaders() + response = conn.response_class(conn.sock, method='POST') + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + assert status == 100 + skip = True + while skip: + skip = response.fp.readline().strip() + + # ...send the body + conn.send(b'x' * 1000) + + # ...get the final response + response.begin() + status_line, actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + assert actual_status == 500 + + # Now try a working page with an Expect header... + conn._output(b'POST /upload HTTP/1.1') + conn._output(('Host: %s' % conn.host).encode('ascii')) + conn._output(b'Content-Type: text/plain') + conn._output(b'Content-Length: 17') + conn._output(b'Expect: 100-continue') + conn._send_output() + response = conn.response_class(conn.sock, method='POST') + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + assert status == 100 + skip = True + while skip: + skip = response.fp.readline().strip() + + # ...send the body + body = b'I am a small file' + conn.send(body) + + # ...get the final response + response.begin() + status_line, actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + assert actual_status == 200 + expected_resp_body = ("thanks for '%s'" % body).encode() + assert actual_resp_body == expected_resp_body + conn.close() + + test_client.server_instance.max_request_body_size = old_max + + +def test_No_Message_Body(test_client): + """Test HTTP queries with an empty response body.""" + # Initialize a persistent HTTP connection + http_connection = test_client.get_connection() + http_connection.auto_open = False + http_connection.connect() + + # Make the first request and assert there's no "Connection: close". + status_line, actual_headers, actual_resp_body = test_client.get( + '/pov', http_conn=http_connection + ) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + assert actual_resp_body == pov.encode() + assert not header_exists('Connection', actual_headers) + + # Make a 204 request on the same connection. + status_line, actual_headers, actual_resp_body = test_client.get( + '/custom/204', http_conn=http_connection + ) + actual_status = int(status_line[:3]) + assert actual_status == 204 + assert not header_exists('Content-Length', actual_headers) + assert actual_resp_body == b'' + assert not header_exists('Connection', actual_headers) + + # Make a 304 request on the same connection. + status_line, actual_headers, actual_resp_body = test_client.get( + '/custom/304', http_conn=http_connection + ) + actual_status = int(status_line[:3]) + assert actual_status == 304 + assert not header_exists('Content-Length', actual_headers) + assert actual_resp_body == b'' + assert not header_exists('Connection', actual_headers) + + +@pytest.mark.xfail( + reason='Server does not correctly read trailers/ending of the previous ' + 'HTTP request, thus the second request fails as the server tries ' + r"to parse b'Content-Type: application/json\r\n' as a " + 'Request-Line. This results in HTTP status code 400, instead of 413' + 'Ref: https://github.com/cherrypy/cheroot/issues/69' +) +def test_Chunked_Encoding(test_client): + """Test HTTP uploads with chunked transfer-encoding.""" + # Initialize a persistent HTTP connection + conn = test_client.get_connection() + + # Try a normal chunked request (with extensions) + body = ( + b'8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n' + b'Content-Type: application/json\r\n' + b'\r\n' + ) + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', conn.host) + conn.putheader('Transfer-Encoding', 'chunked') + conn.putheader('Trailer', 'Content-Type') + # Note that this is somewhat malformed: + # we shouldn't be sending Content-Length. + # RFC 2616 says the server should ignore it. + conn.putheader('Content-Length', '3') + conn.endheaders() + conn.send(body) + response = conn.getresponse() + status_line, actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + assert actual_status == 200 + assert status_line[4:] == 'OK' + expected_resp_body = ("thanks for '%s'" % b'xx\r\nxxxxyyyyy').encode() + assert actual_resp_body == expected_resp_body + + # Try a chunked request that exceeds server.max_request_body_size. + # Note that the delimiters and trailer are included. + body = b'3e3\r\n' + (b'x' * 995) + b'\r\n0\r\n\r\n' + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', conn.host) + conn.putheader('Transfer-Encoding', 'chunked') + conn.putheader('Content-Type', 'text/plain') + # Chunked requests don't need a content-length + # conn.putheader("Content-Length", len(body)) + conn.endheaders() + conn.send(body) + response = conn.getresponse() + status_line, actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + assert actual_status == 413 + conn.close() + + +def test_Content_Length_in(test_client): + """Try a non-chunked request where Content-Length exceeds limit. + + (server.max_request_body_size). + Assert error before body send. + """ + # Initialize a persistent HTTP connection + conn = test_client.get_connection() + + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', conn.host) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '9999') + conn.endheaders() + response = conn.getresponse() + status_line, actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + assert actual_status == 413 + expected_resp_body = ( + b'The entity sent with the request exceeds ' + b'the maximum allowed bytes.' + ) + assert actual_resp_body == expected_resp_body + conn.close() + + +def test_Content_Length_not_int(test_client): + """Test that malicious Content-Length header returns 400.""" + status_line, actual_headers, actual_resp_body = test_client.post( + '/upload', + headers=[ + ('Content-Type', 'text/plain'), + ('Content-Length', 'not-an-integer'), + ], + ) + actual_status = int(status_line[:3]) + + assert actual_status == 400 + assert actual_resp_body == b'Malformed Content-Length Header.' + + +@pytest.mark.parametrize( + 'uri,expected_resp_status,expected_resp_body', + ( + ('/wrong_cl_buffered', 500, + (b'The requested resource returned more bytes than the ' + b'declared Content-Length.')), + ('/wrong_cl_unbuffered', 200, b'I too'), + ) +) +def test_Content_Length_out( + test_client, + uri, expected_resp_status, expected_resp_body +): + """Test response with Content-Length less than the response body. + + (non-chunked response) + """ + conn = test_client.get_connection() + conn.putrequest('GET', uri, skip_host=True) + conn.putheader('Host', conn.host) + conn.endheaders() + + response = conn.getresponse() + status_line, actual_headers, actual_resp_body = webtest.shb(response) + actual_status = int(status_line[:3]) + + assert actual_status == expected_resp_status + assert actual_resp_body == expected_resp_body + + conn.close() + + +@pytest.mark.xfail( + reason='Sometimes this test fails due to low timeout. ' + 'Ref: https://github.com/cherrypy/cherrypy/issues/598' +) +def test_598(test_client): + """Test serving large file with a read timeout in place.""" + # Initialize a persistent HTTP connection + conn = test_client.get_connection() + remote_data_conn = urllib.request.urlopen( + '%s://%s:%s/one_megabyte_of_a' + % ('http', conn.host, conn.port) + ) + buf = remote_data_conn.read(512) + time.sleep(timeout * 0.6) + remaining = (1024 * 1024) - 512 + while remaining: + data = remote_data_conn.read(remaining) + if not data: + break + buf += data + remaining -= len(data) + + assert len(buf) == 1024 * 1024 + assert buf == b'a' * 1024 * 1024 + assert remaining == 0 + remote_data_conn.close() + + +@pytest.mark.parametrize( + 'invalid_terminator', + ( + b'\n\n', + b'\r\n\n', + ) +) +def test_No_CRLF(test_client, invalid_terminator): + """Test HTTP queries with no valid CRLF terminators.""" + # Initialize a persistent HTTP connection + conn = test_client.get_connection() + + # (b'%s' % b'') is not supported in Python 3.4, so just use + + conn.send(b'GET /hello HTTP/1.1' + invalid_terminator) + response = conn.response_class(conn.sock, method='GET') + response.begin() + actual_resp_body = response.read() + expected_resp_body = b'HTTP requires CRLF terminators' + assert actual_resp_body == expected_resp_body + conn.close() diff --git a/resources/lib/cheroot/test/test_core.py b/resources/lib/cheroot/test/test_core.py new file mode 100644 index 0000000..7c91b13 --- /dev/null +++ b/resources/lib/cheroot/test/test_core.py @@ -0,0 +1,405 @@ +"""Tests for managing HTTP issues (malformed requests, etc).""" +# -*- coding: utf-8 -*- +# vim: set fileencoding=utf-8 : + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import errno +import socket + +import pytest +import six +from six.moves import urllib + +from cheroot.test import helper + + +HTTP_BAD_REQUEST = 400 +HTTP_LENGTH_REQUIRED = 411 +HTTP_NOT_FOUND = 404 +HTTP_OK = 200 +HTTP_VERSION_NOT_SUPPORTED = 505 + + +class HelloController(helper.Controller): + """Controller for serving WSGI apps.""" + + def hello(req, resp): + """Render Hello world.""" + return 'Hello world!' + + def body_required(req, resp): + """Render Hello world or set 411.""" + if req.environ.get('Content-Length', None) is None: + resp.status = '411 Length Required' + return + return 'Hello world!' + + def query_string(req, resp): + """Render QUERY_STRING value.""" + return req.environ.get('QUERY_STRING', '') + + def asterisk(req, resp): + """Render request method value.""" + method = req.environ.get('REQUEST_METHOD', 'NO METHOD FOUND') + tmpl = 'Got asterisk URI path with {method} method' + return tmpl.format(**locals()) + + def _munge(string): + """Encode PATH_INFO correctly depending on Python version. + + WSGI 1.0 is a mess around unicode. Create endpoints + that match the PATH_INFO that it produces. + """ + if six.PY3: + return string.encode('utf-8').decode('latin-1') + return string + + handlers = { + '/hello': hello, + '/no_body': hello, + '/body_required': body_required, + '/query_string': query_string, + _munge('/привіт'): hello, + _munge('/Юххууу'): hello, + '/\xa0Ðblah key 0 900 4 data': hello, + '/*': asterisk, + } + + +def _get_http_response(connection, method='GET'): + c = connection + kwargs = {'strict': c.strict} if hasattr(c, 'strict') else {} + # Python 3.2 removed the 'strict' feature, saying: + # "http.client now always assumes HTTP/1.x compliant servers." + return c.response_class(c.sock, method=method, **kwargs) + + +@pytest.fixture +def testing_server(wsgi_server_client): + """Attach a WSGI app to the given server and pre-configure it.""" + wsgi_server = wsgi_server_client.server_instance + wsgi_server.wsgi_app = HelloController() + wsgi_server.max_request_body_size = 30000000 + wsgi_server.server_client = wsgi_server_client + return wsgi_server + + +@pytest.fixture +def test_client(testing_server): + """Get and return a test client out of the given server.""" + return testing_server.server_client + + +def test_http_connect_request(test_client): + """Check that CONNECT query results in Method Not Allowed status.""" + status_line = test_client.connect('/anything')[0] + actual_status = int(status_line[:3]) + assert actual_status == 405 + + +def test_normal_request(test_client): + """Check that normal GET query succeeds.""" + status_line, _, actual_resp_body = test_client.get('/hello') + actual_status = int(status_line[:3]) + assert actual_status == HTTP_OK + assert actual_resp_body == b'Hello world!' + + +def test_query_string_request(test_client): + """Check that GET param is parsed well.""" + status_line, _, actual_resp_body = test_client.get( + '/query_string?test=True' + ) + actual_status = int(status_line[:3]) + assert actual_status == HTTP_OK + assert actual_resp_body == b'test=True' + + +@pytest.mark.parametrize( + 'uri', + ( + '/hello', # plain + '/query_string?test=True', # query + '/{0}?{1}={2}'.format( # quoted unicode + *map(urllib.parse.quote, ('Юххууу', 'ї', 'йо')) + ), + ) +) +def test_parse_acceptable_uri(test_client, uri): + """Check that server responds with OK to valid GET queries.""" + status_line = test_client.get(uri)[0] + actual_status = int(status_line[:3]) + assert actual_status == HTTP_OK + + +@pytest.mark.xfail(six.PY2, reason='Fails on Python 2') +def test_parse_uri_unsafe_uri(test_client): + """Test that malicious URI does not allow HTTP injection. + + This effectively checks that sending GET request with URL + + /%A0%D0blah%20key%200%20900%204%20data + + is not converted into + + GET / + blah key 0 900 4 data + HTTP/1.1 + + which would be a security issue otherwise. + """ + c = test_client.get_connection() + resource = '/\xa0Ðblah key 0 900 4 data'.encode('latin-1') + quoted = urllib.parse.quote(resource) + assert quoted == '/%A0%D0blah%20key%200%20900%204%20data' + request = 'GET {quoted} HTTP/1.1'.format(**locals()) + c._output(request.encode('utf-8')) + c._send_output() + response = _get_http_response(c, method='GET') + response.begin() + assert response.status == HTTP_OK + assert response.fp.read(12) == b'Hello world!' + c.close() + + +def test_parse_uri_invalid_uri(test_client): + """Check that server responds with Bad Request to invalid GET queries. + + Invalid request line test case: it should only contain US-ASCII. + """ + c = test_client.get_connection() + c._output(u'GET /йопта! HTTP/1.1'.encode('utf-8')) + c._send_output() + response = _get_http_response(c, method='GET') + response.begin() + assert response.status == HTTP_BAD_REQUEST + assert response.fp.read(21) == b'Malformed Request-URI' + c.close() + + +@pytest.mark.parametrize( + 'uri', + ( + 'hello', # ascii + 'привіт', # non-ascii + ) +) +def test_parse_no_leading_slash_invalid(test_client, uri): + """Check that server responds with Bad Request to invalid GET queries. + + Invalid request line test case: it should have leading slash (be absolute). + """ + status_line, _, actual_resp_body = test_client.get( + urllib.parse.quote(uri) + ) + actual_status = int(status_line[:3]) + assert actual_status == HTTP_BAD_REQUEST + assert b'starting with a slash' in actual_resp_body + + +def test_parse_uri_absolute_uri(test_client): + """Check that server responds with Bad Request to Absolute URI. + + Only proxy servers should allow this. + """ + status_line, _, actual_resp_body = test_client.get('http://google.com/') + actual_status = int(status_line[:3]) + assert actual_status == HTTP_BAD_REQUEST + expected_body = b'Absolute URI not allowed if server is not a proxy.' + assert actual_resp_body == expected_body + + +def test_parse_uri_asterisk_uri(test_client): + """Check that server responds with OK to OPTIONS with "*" Absolute URI.""" + status_line, _, actual_resp_body = test_client.options('*') + actual_status = int(status_line[:3]) + assert actual_status == HTTP_OK + expected_body = b'Got asterisk URI path with OPTIONS method' + assert actual_resp_body == expected_body + + +def test_parse_uri_fragment_uri(test_client): + """Check that server responds with Bad Request to URI with fragment.""" + status_line, _, actual_resp_body = test_client.get( + '/hello?test=something#fake', + ) + actual_status = int(status_line[:3]) + assert actual_status == HTTP_BAD_REQUEST + expected_body = b'Illegal #fragment in Request-URI.' + assert actual_resp_body == expected_body + + +def test_no_content_length(test_client): + """Test POST query with an empty body being successful.""" + # "The presence of a message-body in a request is signaled by the + # inclusion of a Content-Length or Transfer-Encoding header field in + # the request's message-headers." + # + # Send a message with neither header and no body. + c = test_client.get_connection() + c.request('POST', '/no_body') + response = c.getresponse() + actual_resp_body = response.fp.read() + actual_status = response.status + assert actual_status == HTTP_OK + assert actual_resp_body == b'Hello world!' + + +def test_content_length_required(test_client): + """Test POST query with body failing because of missing Content-Length.""" + # Now send a message that has no Content-Length, but does send a body. + # Verify that CP times out the socket and responds + # with 411 Length Required. + + c = test_client.get_connection() + c.request('POST', '/body_required') + response = c.getresponse() + response.fp.read() + + actual_status = response.status + assert actual_status == HTTP_LENGTH_REQUIRED + + +@pytest.mark.parametrize( + 'request_line,status_code,expected_body', + ( + (b'GET /', # missing proto + HTTP_BAD_REQUEST, b'Malformed Request-Line'), + (b'GET / HTTPS/1.1', # invalid proto + HTTP_BAD_REQUEST, b'Malformed Request-Line: bad protocol'), + (b'GET / HTTP/2.15', # invalid ver + HTTP_VERSION_NOT_SUPPORTED, b'Cannot fulfill request'), + ) +) +def test_malformed_request_line( + test_client, request_line, + status_code, expected_body +): + """Test missing or invalid HTTP version in Request-Line.""" + c = test_client.get_connection() + c._output(request_line) + c._send_output() + response = _get_http_response(c, method='GET') + response.begin() + assert response.status == status_code + assert response.fp.read(len(expected_body)) == expected_body + c.close() + + +def test_malformed_http_method(test_client): + """Test non-uppercase HTTP method.""" + c = test_client.get_connection() + c.putrequest('GeT', '/malformed_method_case') + c.putheader('Content-Type', 'text/plain') + c.endheaders() + + response = c.getresponse() + actual_status = response.status + assert actual_status == HTTP_BAD_REQUEST + actual_resp_body = response.fp.read(21) + assert actual_resp_body == b'Malformed method name' + + +def test_malformed_header(test_client): + """Check that broken HTTP header results in Bad Request.""" + c = test_client.get_connection() + c.putrequest('GET', '/') + c.putheader('Content-Type', 'text/plain') + # See https://www.bitbucket.org/cherrypy/cherrypy/issue/941 + c._output(b'Re, 1.2.3.4#015#012') + c.endheaders() + + response = c.getresponse() + actual_status = response.status + assert actual_status == HTTP_BAD_REQUEST + actual_resp_body = response.fp.read(20) + assert actual_resp_body == b'Illegal header line.' + + +def test_request_line_split_issue_1220(test_client): + """Check that HTTP request line of exactly 256 chars length is OK.""" + Request_URI = ( + '/hello?' + 'intervenant-entreprise-evenement_classaction=' + 'evenement-mailremerciements' + '&_path=intervenant-entreprise-evenement' + '&intervenant-entreprise-evenement_action-id=19404' + '&intervenant-entreprise-evenement_id=19404' + '&intervenant-entreprise_id=28092' + ) + assert len('GET %s HTTP/1.1\r\n' % Request_URI) == 256 + + actual_resp_body = test_client.get(Request_URI)[2] + assert actual_resp_body == b'Hello world!' + + +def test_garbage_in(test_client): + """Test that server sends an error for garbage received over TCP.""" + # Connect without SSL regardless of server.scheme + + c = test_client.get_connection() + c._output(b'gjkgjklsgjklsgjkljklsg') + c._send_output() + response = c.response_class(c.sock, method='GET') + try: + response.begin() + actual_status = response.status + assert actual_status == HTTP_BAD_REQUEST + actual_resp_body = response.fp.read(22) + assert actual_resp_body == b'Malformed Request-Line' + c.close() + except socket.error as ex: + # "Connection reset by peer" is also acceptable. + if ex.errno != errno.ECONNRESET: + raise + + +class CloseController: + """Controller for testing the close callback.""" + + def __call__(self, environ, start_response): + """Get the req to know header sent status.""" + self.req = start_response.__self__.req + resp = CloseResponse(self.close) + start_response(resp.status, resp.headers.items()) + return resp + + def close(self): + """Close, writing hello.""" + self.req.write(b'hello') + + +class CloseResponse: + """Dummy empty response to trigger the no body status.""" + + def __init__(self, close): + """Use some defaults to ensure we have a header.""" + self.status = '200 OK' + self.headers = {'Content-Type': 'text/html'} + self.close = close + + def __getitem__(self, index): + """Ensure we don't have a body.""" + raise IndexError() + + def output(self): + """Return self to hook the close method.""" + return self + + +@pytest.fixture +def testing_server_close(wsgi_server_client): + """Attach a WSGI app to the given server and pre-configure it.""" + wsgi_server = wsgi_server_client.server_instance + wsgi_server.wsgi_app = CloseController() + wsgi_server.max_request_body_size = 30000000 + wsgi_server.server_client = wsgi_server_client + return wsgi_server + + +def test_send_header_before_closing(testing_server_close): + """Test we are actually sending the headers before calling 'close'.""" + _, _, resp_body = testing_server_close.server_client.get('/') + assert resp_body == b'hello' diff --git a/resources/lib/cheroot/test/test_server.py b/resources/lib/cheroot/test/test_server.py new file mode 100644 index 0000000..c53f7a8 --- /dev/null +++ b/resources/lib/cheroot/test/test_server.py @@ -0,0 +1,193 @@ +"""Tests for the HTTP server.""" +# -*- coding: utf-8 -*- +# vim: set fileencoding=utf-8 : + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import os +import socket +import tempfile +import threading +import time + +import pytest + +from .._compat import bton +from ..server import Gateway, HTTPServer +from ..testing import ( + ANY_INTERFACE_IPV4, + ANY_INTERFACE_IPV6, + EPHEMERAL_PORT, + get_server_client, +) + + +def make_http_server(bind_addr): + """Create and start an HTTP server bound to bind_addr.""" + httpserver = HTTPServer( + bind_addr=bind_addr, + gateway=Gateway, + ) + + threading.Thread(target=httpserver.safe_start).start() + + while not httpserver.ready: + time.sleep(0.1) + + return httpserver + + +non_windows_sock_test = pytest.mark.skipif( + not hasattr(socket, 'AF_UNIX'), + reason='UNIX domain sockets are only available under UNIX-based OS', +) + + +@pytest.fixture +def http_server(): + """Provision a server creator as a fixture.""" + def start_srv(): + bind_addr = yield + httpserver = make_http_server(bind_addr) + yield httpserver + yield httpserver + + srv_creator = iter(start_srv()) + next(srv_creator) + yield srv_creator + try: + while True: + httpserver = next(srv_creator) + if httpserver is not None: + httpserver.stop() + except StopIteration: + pass + + +@pytest.fixture +def unix_sock_file(): + """Check that bound UNIX socket address is stored in server.""" + tmp_sock_fh, tmp_sock_fname = tempfile.mkstemp() + + yield tmp_sock_fname + + os.close(tmp_sock_fh) + os.unlink(tmp_sock_fname) + + +def test_prepare_makes_server_ready(): + """Check that prepare() makes the server ready, and stop() clears it.""" + httpserver = HTTPServer( + bind_addr=(ANY_INTERFACE_IPV4, EPHEMERAL_PORT), + gateway=Gateway, + ) + + assert not httpserver.ready + assert not httpserver.requests._threads + + httpserver.prepare() + + assert httpserver.ready + assert httpserver.requests._threads + for thr in httpserver.requests._threads: + assert thr.ready + + httpserver.stop() + + assert not httpserver.requests._threads + assert not httpserver.ready + + +def test_stop_interrupts_serve(): + """Check that stop() interrupts running of serve().""" + httpserver = HTTPServer( + bind_addr=(ANY_INTERFACE_IPV4, EPHEMERAL_PORT), + gateway=Gateway, + ) + + httpserver.prepare() + serve_thread = threading.Thread(target=httpserver.serve) + serve_thread.start() + + serve_thread.join(0.5) + assert serve_thread.is_alive() + + httpserver.stop() + + serve_thread.join(0.5) + assert not serve_thread.is_alive() + + +@pytest.mark.parametrize( + 'ip_addr', + ( + ANY_INTERFACE_IPV4, + ANY_INTERFACE_IPV6, + ) +) +def test_bind_addr_inet(http_server, ip_addr): + """Check that bound IP address is stored in server.""" + httpserver = http_server.send((ip_addr, EPHEMERAL_PORT)) + + assert httpserver.bind_addr[0] == ip_addr + assert httpserver.bind_addr[1] != EPHEMERAL_PORT + + +@non_windows_sock_test +def test_bind_addr_unix(http_server, unix_sock_file): + """Check that bound UNIX socket address is stored in server.""" + httpserver = http_server.send(unix_sock_file) + + assert httpserver.bind_addr == unix_sock_file + + +@pytest.mark.skip(reason="Abstract sockets don't work currently") +@non_windows_sock_test +def test_bind_addr_unix_abstract(http_server): + """Check that bound UNIX socket address is stored in server.""" + unix_abstract_sock = b'\x00cheroot/test/socket/here.sock' + httpserver = http_server.send(unix_abstract_sock) + + assert httpserver.bind_addr == unix_abstract_sock + + +PEERCRED_IDS_URI = '/peer_creds/ids' +PEERCRED_TEXTS_URI = '/peer_creds/texts' + + +class _TestGateway(Gateway): + def respond(self): + req = self.req + conn = req.conn + req_uri = bton(req.uri) + if req_uri == PEERCRED_IDS_URI: + peer_creds = conn.peer_pid, conn.peer_uid, conn.peer_gid + return ['|'.join(map(str, peer_creds))] + elif req_uri == PEERCRED_TEXTS_URI: + return ['!'.join((conn.peer_user, conn.peer_group))] + return super(_TestGateway, self).respond() + + +@pytest.mark.skip( + reason='Test HTTP client is not able to work through UNIX socket currently' +) +@non_windows_sock_test +def test_peercreds_unix_sock(http_server, unix_sock_file): + """Check that peercred lookup and resolution work when enabled.""" + httpserver = http_server.send(unix_sock_file) + httpserver.gateway = _TestGateway + httpserver.peercreds_enabled = True + + testclient = get_server_client(httpserver) + + expected_peercreds = os.getpid(), os.getuid(), os.getgid() + expected_peercreds = '|'.join(map(str, expected_peercreds)) + assert testclient.get(PEERCRED_IDS_URI) == expected_peercreds + assert 'RuntimeError' in testclient.get(PEERCRED_TEXTS_URI) + + httpserver.peercreds_resolve_enabled = True + import grp + expected_textcreds = os.getlogin(), grp.getgrgid(os.getgid()).gr_name + expected_textcreds = '!'.join(map(str, expected_textcreds)) + assert testclient.get(PEERCRED_TEXTS_URI) == expected_textcreds diff --git a/resources/lib/cheroot/test/webtest.py b/resources/lib/cheroot/test/webtest.py new file mode 100644 index 0000000..43448f5 --- /dev/null +++ b/resources/lib/cheroot/test/webtest.py @@ -0,0 +1,581 @@ +"""Extensions to unittest for web frameworks. + +Use the WebCase.getPage method to request a page from your HTTP server. +Framework Integration +===================== +If you have control over your server process, you can handle errors +in the server-side of the HTTP conversation a bit better. You must run +both the client (your WebCase tests) and the server in the same process +(but in separate threads, obviously). +When an error occurs in the framework, call server_error. It will print +the traceback to stdout, and keep any assertions you have from running +(the assumption is that, if the server errors, the page output will not +be of further significance to your tests). +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import pprint +import re +import socket +import sys +import time +import traceback +import os +import json +import unittest +import warnings + +from six.moves import range, http_client, map, urllib_parse +import six + +from more_itertools.more import always_iterable + + +def interface(host): + """Return an IP address for a client connection given the server host. + + If the server is listening on '0.0.0.0' (INADDR_ANY) + or '::' (IN6ADDR_ANY), this will return the proper localhost. + """ + if host == '0.0.0.0': + # INADDR_ANY, which should respond on localhost. + return '127.0.0.1' + if host == '::': + # IN6ADDR_ANY, which should respond on localhost. + return '::1' + return host + + +try: + # Jython support + if sys.platform[:4] == 'java': + def getchar(): + """Get a key press.""" + # Hopefully this is enough + return sys.stdin.read(1) + else: + # On Windows, msvcrt.getch reads a single char without output. + import msvcrt + + def getchar(): + """Get a key press.""" + return msvcrt.getch() +except ImportError: + # Unix getchr + import tty + import termios + + def getchar(): + """Get a key press.""" + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch + + +# from jaraco.properties +class NonDataProperty: + """Non-data property decorator.""" + + def __init__(self, fget): + """Initialize a non-data property.""" + assert fget is not None, 'fget cannot be none' + assert callable(fget), 'fget must be callable' + self.fget = fget + + def __get__(self, obj, objtype=None): + """Return a class property.""" + if obj is None: + return self + return self.fget(obj) + + +class WebCase(unittest.TestCase): + """Helper web test suite base.""" + + HOST = '127.0.0.1' + PORT = 8000 + HTTP_CONN = http_client.HTTPConnection + PROTOCOL = 'HTTP/1.1' + + scheme = 'http' + url = None + + status = None + headers = None + body = None + + encoding = 'utf-8' + + time = None + + @property + def _Conn(self): + """Return HTTPConnection or HTTPSConnection based on self.scheme. + + * from http.client. + """ + cls_name = '{scheme}Connection'.format(scheme=self.scheme.upper()) + return getattr(http_client, cls_name) + + def get_conn(self, auto_open=False): + """Return a connection to our HTTP server.""" + conn = self._Conn(self.interface(), self.PORT) + # Automatically re-connect? + conn.auto_open = auto_open + conn.connect() + return conn + + def set_persistent(self, on=True, auto_open=False): + """Make our HTTP_CONN persistent (or not). + + If the 'on' argument is True (the default), then self.HTTP_CONN + will be set to an instance of HTTP(S)?Connection + to persist across requests. + As this class only allows for a single open connection, if + self already has an open connection, it will be closed. + """ + try: + self.HTTP_CONN.close() + except (TypeError, AttributeError): + pass + + self.HTTP_CONN = ( + self.get_conn(auto_open=auto_open) + if on + else self._Conn + ) + + @property + def persistent(self): # noqa: D401; irrelevant for properties + """Presense of the persistent HTTP connection.""" + return hasattr(self.HTTP_CONN, '__class__') + + @persistent.setter + def persistent(self, on): + self.set_persistent(on) + + def interface(self): + """Return an IP address for a client connection. + + If the server is listening on '0.0.0.0' (INADDR_ANY) + or '::' (IN6ADDR_ANY), this will return the proper localhost. + """ + return interface(self.HOST) + + def getPage(self, url, headers=None, method='GET', body=None, + protocol=None, raise_subcls=None): + """Open the url with debugging support. Return status, headers, body. + + url should be the identifier passed to the server, typically a + server-absolute path and query string (sent between method and + protocol), and should only be an absolute URI if proxy support is + enabled in the server. + + If the application under test generates absolute URIs, be sure + to wrap them first with strip_netloc:: + + class MyAppWebCase(WebCase): + def getPage(url, *args, **kwargs): + super(MyAppWebCase, self).getPage( + cheroot.test.webtest.strip_netloc(url), + *args, **kwargs + ) + + `raise_subcls` must be a tuple with the exceptions classes + or a single exception class that are not going to be considered + a socket.error regardless that they were are subclass of a + socket.error and therefore not considered for a connection retry. + """ + ServerError.on = False + + if isinstance(url, six.text_type): + url = url.encode('utf-8') + if isinstance(body, six.text_type): + body = body.encode('utf-8') + + self.url = url + self.time = None + start = time.time() + result = openURL(url, headers, method, body, self.HOST, self.PORT, + self.HTTP_CONN, protocol or self.PROTOCOL, + raise_subcls) + self.time = time.time() - start + self.status, self.headers, self.body = result + + # Build a list of request cookies from the previous response cookies. + self.cookies = [('Cookie', v) for k, v in self.headers + if k.lower() == 'set-cookie'] + + if ServerError.on: + raise ServerError() + return result + + @NonDataProperty + def interactive(self): + """Determine whether tests are run in interactive mode. + + Load interactivity setting from environment, where + the value can be numeric or a string like true or + False or 1 or 0. + """ + env_str = os.environ.get('WEBTEST_INTERACTIVE', 'True') + is_interactive = bool(json.loads(env_str.lower())) + if is_interactive: + warnings.warn( + 'Interactive test failure interceptor support via ' + 'WEBTEST_INTERACTIVE environment variable is deprecated.', + DeprecationWarning + ) + return is_interactive + + console_height = 30 + + def _handlewebError(self, msg): + print('') + print(' ERROR: %s' % msg) + + if not self.interactive: + raise self.failureException(msg) + + p = (' Show: ' + '[B]ody [H]eaders [S]tatus [U]RL; ' + '[I]gnore, [R]aise, or sys.e[X]it >> ') + sys.stdout.write(p) + sys.stdout.flush() + while True: + i = getchar().upper() + if not isinstance(i, type('')): + i = i.decode('ascii') + if i not in 'BHSUIRX': + continue + print(i.upper()) # Also prints new line + if i == 'B': + for x, line in enumerate(self.body.splitlines()): + if (x + 1) % self.console_height == 0: + # The \r and comma should make the next line overwrite + sys.stdout.write('<-- More -->\r') + m = getchar().lower() + # Erase our "More" prompt + sys.stdout.write(' \r') + if m == 'q': + break + print(line) + elif i == 'H': + pprint.pprint(self.headers) + elif i == 'S': + print(self.status) + elif i == 'U': + print(self.url) + elif i == 'I': + # return without raising the normal exception + return + elif i == 'R': + raise self.failureException(msg) + elif i == 'X': + sys.exit() + sys.stdout.write(p) + sys.stdout.flush() + + @property + def status_code(self): # noqa: D401; irrelevant for properties + """Integer HTTP status code.""" + return int(self.status[:3]) + + def status_matches(self, expected): + """Check whether actual status matches expected.""" + actual = ( + self.status_code + if isinstance(expected, int) else + self.status + ) + return expected == actual + + def assertStatus(self, status, msg=None): + """Fail if self.status != status. + + status may be integer code, exact string status, or + iterable of allowed possibilities. + """ + if any(map(self.status_matches, always_iterable(status))): + return + + tmpl = 'Status {self.status} does not match {status}' + msg = msg or tmpl.format(**locals()) + self._handlewebError(msg) + + def assertHeader(self, key, value=None, msg=None): + """Fail if (key, [value]) not in self.headers.""" + lowkey = key.lower() + for k, v in self.headers: + if k.lower() == lowkey: + if value is None or str(value) == v: + return v + + if msg is None: + if value is None: + msg = '%r not in headers' % key + else: + msg = '%r:%r not in headers' % (key, value) + self._handlewebError(msg) + + def assertHeaderIn(self, key, values, msg=None): + """Fail if header indicated by key doesn't have one of the values.""" + lowkey = key.lower() + for k, v in self.headers: + if k.lower() == lowkey: + matches = [value for value in values if str(value) == v] + if matches: + return matches + + if msg is None: + msg = '%(key)r not in %(values)r' % vars() + self._handlewebError(msg) + + def assertHeaderItemValue(self, key, value, msg=None): + """Fail if the header does not contain the specified value.""" + actual_value = self.assertHeader(key, msg=msg) + header_values = map(str.strip, actual_value.split(',')) + if value in header_values: + return value + + if msg is None: + msg = '%r not in %r' % (value, header_values) + self._handlewebError(msg) + + def assertNoHeader(self, key, msg=None): + """Fail if key in self.headers.""" + lowkey = key.lower() + matches = [k for k, v in self.headers if k.lower() == lowkey] + if matches: + if msg is None: + msg = '%r in headers' % key + self._handlewebError(msg) + + def assertNoHeaderItemValue(self, key, value, msg=None): + """Fail if the header contains the specified value.""" + lowkey = key.lower() + hdrs = self.headers + matches = [k for k, v in hdrs if k.lower() == lowkey and v == value] + if matches: + if msg is None: + msg = '%r:%r in %r' % (key, value, hdrs) + self._handlewebError(msg) + + def assertBody(self, value, msg=None): + """Fail if value != self.body.""" + if isinstance(value, six.text_type): + value = value.encode(self.encoding) + if value != self.body: + if msg is None: + msg = 'expected body:\n%r\n\nactual body:\n%r' % ( + value, self.body) + self._handlewebError(msg) + + def assertInBody(self, value, msg=None): + """Fail if value not in self.body.""" + if isinstance(value, six.text_type): + value = value.encode(self.encoding) + if value not in self.body: + if msg is None: + msg = '%r not in body: %s' % (value, self.body) + self._handlewebError(msg) + + def assertNotInBody(self, value, msg=None): + """Fail if value in self.body.""" + if isinstance(value, six.text_type): + value = value.encode(self.encoding) + if value in self.body: + if msg is None: + msg = '%r found in body' % value + self._handlewebError(msg) + + def assertMatchesBody(self, pattern, msg=None, flags=0): + """Fail if value (a regex pattern) is not in self.body.""" + if isinstance(pattern, six.text_type): + pattern = pattern.encode(self.encoding) + if re.search(pattern, self.body, flags) is None: + if msg is None: + msg = 'No match for %r in body' % pattern + self._handlewebError(msg) + + +methods_with_bodies = ('POST', 'PUT', 'PATCH') + + +def cleanHeaders(headers, method, body, host, port): + """Return request headers, with required headers added (if missing).""" + if headers is None: + headers = [] + + # Add the required Host request header if not present. + # [This specifies the host:port of the server, not the client.] + found = False + for k, v in headers: + if k.lower() == 'host': + found = True + break + if not found: + if port == 80: + headers.append(('Host', host)) + else: + headers.append(('Host', '%s:%s' % (host, port))) + + if method in methods_with_bodies: + # Stick in default type and length headers if not present + found = False + for k, v in headers: + if k.lower() == 'content-type': + found = True + break + if not found: + headers.append( + ('Content-Type', 'application/x-www-form-urlencoded')) + headers.append(('Content-Length', str(len(body or '')))) + + return headers + + +def shb(response): + """Return status, headers, body the way we like from a response.""" + if six.PY3: + h = response.getheaders() + else: + h = [] + key, value = None, None + for line in response.msg.headers: + if line: + if line[0] in ' \t': + value += line.strip() + else: + if key and value: + h.append((key, value)) + key, value = line.split(':', 1) + key = key.strip() + value = value.strip() + if key and value: + h.append((key, value)) + + return '%s %s' % (response.status, response.reason), h, response.read() + + +def openURL(url, headers=None, method='GET', body=None, + host='127.0.0.1', port=8000, http_conn=http_client.HTTPConnection, + protocol='HTTP/1.1', raise_subcls=None): + """ + Open the given HTTP resource and return status, headers, and body. + + `raise_subcls` must be a tuple with the exceptions classes + or a single exception class that are not going to be considered + a socket.error regardless that they were are subclass of a + socket.error and therefore not considered for a connection retry. + """ + headers = cleanHeaders(headers, method, body, host, port) + + # Trying 10 times is simply in case of socket errors. + # Normal case--it should run once. + for trial in range(10): + try: + # Allow http_conn to be a class or an instance + if hasattr(http_conn, 'host'): + conn = http_conn + else: + conn = http_conn(interface(host), port) + + conn._http_vsn_str = protocol + conn._http_vsn = int(''.join([x for x in protocol if x.isdigit()])) + + if six.PY3 and isinstance(url, bytes): + url = url.decode() + conn.putrequest(method.upper(), url, skip_host=True, + skip_accept_encoding=True) + + for key, value in headers: + conn.putheader(key, value.encode('Latin-1')) + conn.endheaders() + + if body is not None: + conn.send(body) + + # Handle response + response = conn.getresponse() + + s, h, b = shb(response) + + if not hasattr(http_conn, 'host'): + # We made our own conn instance. Close it. + conn.close() + + return s, h, b + except socket.error as e: + if raise_subcls is not None and isinstance(e, raise_subcls): + raise + else: + time.sleep(0.5) + if trial == 9: + raise + + +def strip_netloc(url): + """Return absolute-URI path from URL. + + Strip the scheme and host from the URL, returning the + server-absolute portion. + + Useful for wrapping an absolute-URI for which only the + path is expected (such as in calls to getPage). + + >>> strip_netloc('https://google.com/foo/bar?bing#baz') + '/foo/bar?bing' + + >>> strip_netloc('//google.com/foo/bar?bing#baz') + '/foo/bar?bing' + + >>> strip_netloc('/foo/bar?bing#baz') + '/foo/bar?bing' + """ + parsed = urllib_parse.urlparse(url) + scheme, netloc, path, params, query, fragment = parsed + stripped = '', '', path, params, query, '' + return urllib_parse.urlunparse(stripped) + + +# Add any exceptions which your web framework handles +# normally (that you don't want server_error to trap). +ignored_exceptions = [] + +# You'll want set this to True when you can't guarantee +# that each response will immediately follow each request; +# for example, when handling requests via multiple threads. +ignore_all = False + + +class ServerError(Exception): + """Exception for signalling server error.""" + + on = False + + +def server_error(exc=None): + """Server debug hook. + + Return True if exception handled, False if ignored. + You probably want to wrap this, so you can still handle an error using + your framework when it's ignored. + """ + if exc is None: + exc = sys.exc_info() + + if ignore_all or exc[0] in ignored_exceptions: + return False + else: + ServerError.on = True + print('') + print(''.join(traceback.format_exception(*exc))) + return True diff --git a/resources/lib/cheroot/testing.py b/resources/lib/cheroot/testing.py new file mode 100644 index 0000000..f01d0aa --- /dev/null +++ b/resources/lib/cheroot/testing.py @@ -0,0 +1,144 @@ +"""Pytest fixtures and other helpers for doing testing by end-users.""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +from contextlib import closing +import errno +import socket +import threading +import time + +import pytest +from six.moves import http_client + +import cheroot.server +from cheroot.test import webtest +import cheroot.wsgi + +EPHEMERAL_PORT = 0 +NO_INTERFACE = None # Using this or '' will cause an exception +ANY_INTERFACE_IPV4 = '0.0.0.0' +ANY_INTERFACE_IPV6 = '::' + +config = { + cheroot.wsgi.Server: { + 'bind_addr': (NO_INTERFACE, EPHEMERAL_PORT), + 'wsgi_app': None, + }, + cheroot.server.HTTPServer: { + 'bind_addr': (NO_INTERFACE, EPHEMERAL_PORT), + 'gateway': cheroot.server.Gateway, + }, +} + + +def cheroot_server(server_factory): + """Set up and tear down a Cheroot server instance.""" + conf = config[server_factory].copy() + bind_port = conf.pop('bind_addr')[-1] + + for interface in ANY_INTERFACE_IPV6, ANY_INTERFACE_IPV4: + try: + actual_bind_addr = (interface, bind_port) + httpserver = server_factory( # create it + bind_addr=actual_bind_addr, + **conf + ) + except OSError: + pass + else: + break + + threading.Thread(target=httpserver.safe_start).start() # spawn it + while not httpserver.ready: # wait until fully initialized and bound + time.sleep(0.1) + + yield httpserver + + httpserver.stop() # destroy it + + +@pytest.fixture(scope='module') +def wsgi_server(): + """Set up and tear down a Cheroot WSGI server instance.""" + for srv in cheroot_server(cheroot.wsgi.Server): + yield srv + + +@pytest.fixture(scope='module') +def native_server(): + """Set up and tear down a Cheroot HTTP server instance.""" + for srv in cheroot_server(cheroot.server.HTTPServer): + yield srv + + +class _TestClient: + def __init__(self, server): + self._interface, self._host, self._port = _get_conn_data(server) + self._http_connection = self.get_connection() + self.server_instance = server + + def get_connection(self): + name = '{interface}:{port}'.format( + interface=self._interface, + port=self._port, + ) + return http_client.HTTPConnection(name) + + def request( + self, uri, method='GET', headers=None, http_conn=None, + protocol='HTTP/1.1', + ): + return webtest.openURL( + uri, method=method, + headers=headers, + host=self._host, port=self._port, + http_conn=http_conn or self._http_connection, + protocol=protocol, + ) + + def __getattr__(self, attr_name): + def _wrapper(uri, **kwargs): + http_method = attr_name.upper() + return self.request(uri, method=http_method, **kwargs) + + return _wrapper + + +def _probe_ipv6_sock(interface): + # Alternate way is to check IPs on interfaces using glibc, like: + # github.com/Gautier/minifail/blob/master/minifail/getifaddrs.py + try: + with closing(socket.socket(family=socket.AF_INET6)) as sock: + sock.bind((interface, 0)) + except (OSError, socket.error) as sock_err: + # In Python 3 socket.error is an alias for OSError + # In Python 2 socket.error is a subclass of IOError + if sock_err.errno != errno.EADDRNOTAVAIL: + raise + else: + return True + + return False + + +def _get_conn_data(server): + if isinstance(server.bind_addr, tuple): + host, port = server.bind_addr + else: + host, port = server.bind_addr, 0 + + interface = webtest.interface(host) + + if ':' in interface and not _probe_ipv6_sock(interface): + interface = '127.0.0.1' + if ':' in host: + host = interface + + return interface, host, port + + +def get_server_client(server): + """Create and return a test client for the given server.""" + return _TestClient(server) diff --git a/resources/lib/cheroot/workers/__init__.py b/resources/lib/cheroot/workers/__init__.py new file mode 100644 index 0000000..098b8f2 --- /dev/null +++ b/resources/lib/cheroot/workers/__init__.py @@ -0,0 +1 @@ +"""HTTP workers pool.""" diff --git a/resources/lib/cheroot/workers/__pycache__/__init__.cpython-37.opt-1.pyc b/resources/lib/cheroot/workers/__pycache__/__init__.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..12d5b1a8019f8508d3abd702fd24138928ded017 GIT binary patch literal 220 zcmZ?b<>g`kg6noC<0OIfV-N=h7=a82ATH(r5-AK(3@MDk44O<;LLMO@0Se{$McJuE z#R>)a`8j%invA#DCRR$p~XOTF*(ImF^&ZVE{P?HF+ur> zxtV$CG2ZzpnK6keDfxNDF$FoL>6v+YiKQu-`Fh0#`6ZcYl`%!B#rdU0$*Dl?nMpCp p8L372`6V$>t7GEhGxIV_;^XxSDsOSvTk{82~ApLDc{N literal 0 HcmV?d00001 diff --git a/resources/lib/cheroot/workers/__pycache__/threadpool.cpython-37.opt-1.pyc b/resources/lib/cheroot/workers/__pycache__/threadpool.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ead052e1cc24197d59b2908522e2297f3765e66e GIT binary patch literal 8031 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 0: + budget = max(self.max - len(self._threads), 0) + else: + # self.max <= 0 indicates no maximum + budget = float('inf') + + n_new = min(amount, budget) + + workers = [self._spawn_worker() for i in range(n_new)] + while not all(worker.ready for worker in workers): + time.sleep(.1) + self._threads.extend(workers) + + def _spawn_worker(self): + worker = WorkerThread(self.server) + worker.setName('CP Server ' + worker.getName()) + worker.start() + return worker + + def shrink(self, amount): + """Kill off worker threads (not below self.min).""" + # Grow/shrink the pool if necessary. + # Remove any dead threads from our list + for t in self._threads: + if not t.isAlive(): + self._threads.remove(t) + amount -= 1 + + # calculate the number of threads above the minimum + n_extra = max(len(self._threads) - self.min, 0) + + # don't remove more than amount + n_to_remove = min(amount, n_extra) + + # put shutdown requests on the queue equal to the number of threads + # to remove. As each request is processed by a worker, that worker + # will terminate and be culled from the list. + for n in range(n_to_remove): + self._queue.put(_SHUTDOWNREQUEST) + + def stop(self, timeout=5): + """Terminate all worker threads. + + Args: + timeout (int): time to wait for threads to stop gracefully + """ + # Must shut down threads here so the code that calls + # this method can know when all threads are stopped. + for worker in self._threads: + self._queue.put(_SHUTDOWNREQUEST) + + # Don't join currentThread (when stop is called inside a request). + current = threading.currentThread() + if timeout is not None and timeout >= 0: + endtime = time.time() + timeout + while self._threads: + worker = self._threads.pop() + if worker is not current and worker.isAlive(): + try: + if timeout is None or timeout < 0: + worker.join() + else: + remaining_time = endtime - time.time() + if remaining_time > 0: + worker.join(remaining_time) + if worker.isAlive(): + # We exhausted the timeout. + # Forcibly shut down the socket. + c = worker.conn + if c and not c.rfile.closed: + try: + c.socket.shutdown(socket.SHUT_RD) + except TypeError: + # pyOpenSSL sockets don't take an arg + c.socket.shutdown() + worker.join() + except (AssertionError, + # Ignore repeated Ctrl-C. + # See + # https://github.com/cherrypy/cherrypy/issues/691. + KeyboardInterrupt): + pass + + @property + def qsize(self): + """Return the queue size.""" + return self._queue.qsize() diff --git a/resources/lib/cheroot/wsgi.py b/resources/lib/cheroot/wsgi.py new file mode 100644 index 0000000..a04c943 --- /dev/null +++ b/resources/lib/cheroot/wsgi.py @@ -0,0 +1,423 @@ +"""This class holds Cheroot WSGI server implementation. + +Simplest example on how to use this server:: + + from cheroot import wsgi + + def my_crazy_app(environ, start_response): + status = '200 OK' + response_headers = [('Content-type','text/plain')] + start_response(status, response_headers) + return [b'Hello world!'] + + addr = '0.0.0.0', 8070 + server = wsgi.Server(addr, my_crazy_app) + server.start() + +The Cheroot WSGI server can serve as many WSGI applications +as you want in one instance by using a PathInfoDispatcher:: + + path_map = { + '/': my_crazy_app, + '/blog': my_blog_app, + } + d = wsgi.PathInfoDispatcher(path_map) + server = wsgi.Server(addr, d) +""" + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + +import sys + +import six +from six.moves import filter + +from . import server +from .workers import threadpool +from ._compat import ntob, bton + + +class Server(server.HTTPServer): + """A subclass of HTTPServer which calls a WSGI application.""" + + wsgi_version = (1, 0) + """The version of WSGI to produce.""" + + def __init__( + self, bind_addr, wsgi_app, numthreads=10, server_name=None, + max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5, + accepted_queue_size=-1, accepted_queue_timeout=10, + peercreds_enabled=False, peercreds_resolve_enabled=False, + ): + """Initialize WSGI Server instance. + + Args: + bind_addr (tuple): network interface to listen to + wsgi_app (callable): WSGI application callable + numthreads (int): number of threads for WSGI thread pool + server_name (str): web server name to be advertised via + Server HTTP header + max (int): maximum number of worker threads + request_queue_size (int): the 'backlog' arg to + socket.listen(); max queued connections + timeout (int): the timeout in seconds for accepted connections + shutdown_timeout (int): the total time, in seconds, to + wait for worker threads to cleanly exit + accepted_queue_size (int): maximum number of active + requests in queue + accepted_queue_timeout (int): timeout for putting request + into queue + """ + super(Server, self).__init__( + bind_addr, + gateway=wsgi_gateways[self.wsgi_version], + server_name=server_name, + peercreds_enabled=peercreds_enabled, + peercreds_resolve_enabled=peercreds_resolve_enabled, + ) + self.wsgi_app = wsgi_app + self.request_queue_size = request_queue_size + self.timeout = timeout + self.shutdown_timeout = shutdown_timeout + self.requests = threadpool.ThreadPool( + self, min=numthreads or 1, max=max, + accepted_queue_size=accepted_queue_size, + accepted_queue_timeout=accepted_queue_timeout) + + @property + def numthreads(self): + """Set minimum number of threads.""" + return self.requests.min + + @numthreads.setter + def numthreads(self, value): + self.requests.min = value + + +class Gateway(server.Gateway): + """A base class to interface HTTPServer with WSGI.""" + + def __init__(self, req): + """Initialize WSGI Gateway instance with request. + + Args: + req (HTTPRequest): current HTTP request + """ + super(Gateway, self).__init__(req) + self.started_response = False + self.env = self.get_environ() + self.remaining_bytes_out = None + + @classmethod + def gateway_map(cls): + """Create a mapping of gateways and their versions. + + Returns: + dict[tuple[int,int],class]: map of gateway version and + corresponding class + + """ + return dict( + (gw.version, gw) + for gw in cls.__subclasses__() + ) + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version.""" + raise NotImplementedError + + def respond(self): + """Process the current request. + + From :pep:`333`: + + The start_response callable must not actually transmit + the response headers. Instead, it must store them for the + server or gateway to transmit only after the first + iteration of the application return value that yields + a NON-EMPTY string, or upon the application's first + invocation of the write() callable. + """ + response = self.req.server.wsgi_app(self.env, self.start_response) + try: + for chunk in filter(None, response): + if not isinstance(chunk, six.binary_type): + raise ValueError('WSGI Applications must yield bytes') + self.write(chunk) + finally: + # Send headers if not already sent + self.req.ensure_headers_sent() + if hasattr(response, 'close'): + response.close() + + def start_response(self, status, headers, exc_info=None): + """WSGI callable to begin the HTTP response.""" + # "The application may call start_response more than once, + # if and only if the exc_info argument is provided." + if self.started_response and not exc_info: + raise AssertionError('WSGI start_response called a second ' + 'time with no exc_info.') + self.started_response = True + + # "if exc_info is provided, and the HTTP headers have already been + # sent, start_response must raise an error, and should raise the + # exc_info tuple." + if self.req.sent_headers: + try: + six.reraise(*exc_info) + finally: + exc_info = None + + self.req.status = self._encode_status(status) + + for k, v in headers: + if not isinstance(k, str): + raise TypeError( + 'WSGI response header key %r is not of type str.' % k) + if not isinstance(v, str): + raise TypeError( + 'WSGI response header value %r is not of type str.' % v) + if k.lower() == 'content-length': + self.remaining_bytes_out = int(v) + out_header = ntob(k), ntob(v) + self.req.outheaders.append(out_header) + + return self.write + + @staticmethod + def _encode_status(status): + """Cast status to bytes representation of current Python version. + + According to :pep:`3333`, when using Python 3, the response status + and headers must be bytes masquerading as unicode; that is, they + must be of type "str" but are restricted to code points in the + "latin-1" set. + """ + if six.PY2: + return status + if not isinstance(status, str): + raise TypeError('WSGI response status is not of type str.') + return status.encode('ISO-8859-1') + + def write(self, chunk): + """WSGI callable to write unbuffered data to the client. + + This method is also used internally by start_response (to write + data from the iterable returned by the WSGI application). + """ + if not self.started_response: + raise AssertionError('WSGI write called before start_response.') + + chunklen = len(chunk) + rbo = self.remaining_bytes_out + if rbo is not None and chunklen > rbo: + if not self.req.sent_headers: + # Whew. We can send a 500 to the client. + self.req.simple_response( + '500 Internal Server Error', + 'The requested resource returned more bytes than the ' + 'declared Content-Length.') + else: + # Dang. We have probably already sent data. Truncate the chunk + # to fit (so the client doesn't hang) and raise an error later. + chunk = chunk[:rbo] + + self.req.ensure_headers_sent() + + self.req.write(chunk) + + if rbo is not None: + rbo -= chunklen + if rbo < 0: + raise ValueError( + 'Response body exceeds the declared Content-Length.') + + +class Gateway_10(Gateway): + """A Gateway class to interface HTTPServer with WSGI 1.0.x.""" + + version = 1, 0 + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version.""" + req = self.req + req_conn = req.conn + env = { + # set a non-standard environ entry so the WSGI app can know what + # the *real* server protocol is (and what features to support). + # See http://www.faqs.org/rfcs/rfc2145.html. + 'ACTUAL_SERVER_PROTOCOL': req.server.protocol, + 'PATH_INFO': bton(req.path), + 'QUERY_STRING': bton(req.qs), + 'REMOTE_ADDR': req_conn.remote_addr or '', + 'REMOTE_PORT': str(req_conn.remote_port or ''), + 'REQUEST_METHOD': bton(req.method), + 'REQUEST_URI': bton(req.uri), + 'SCRIPT_NAME': '', + 'SERVER_NAME': req.server.server_name, + # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol. + 'SERVER_PROTOCOL': bton(req.request_protocol), + 'SERVER_SOFTWARE': req.server.software, + 'wsgi.errors': sys.stderr, + 'wsgi.input': req.rfile, + 'wsgi.input_terminated': bool(req.chunked_read), + 'wsgi.multiprocess': False, + 'wsgi.multithread': True, + 'wsgi.run_once': False, + 'wsgi.url_scheme': bton(req.scheme), + 'wsgi.version': self.version, + } + + if isinstance(req.server.bind_addr, six.string_types): + # AF_UNIX. This isn't really allowed by WSGI, which doesn't + # address unix domain sockets. But it's better than nothing. + env['SERVER_PORT'] = '' + try: + env['X_REMOTE_PID'] = str(req_conn.peer_pid) + env['X_REMOTE_UID'] = str(req_conn.peer_uid) + env['X_REMOTE_GID'] = str(req_conn.peer_gid) + + env['X_REMOTE_USER'] = str(req_conn.peer_user) + env['X_REMOTE_GROUP'] = str(req_conn.peer_group) + + env['REMOTE_USER'] = env['X_REMOTE_USER'] + except RuntimeError: + """Unable to retrieve peer creds data. + + Unsupported by current kernel or socket error happened, or + unsupported socket type, or disabled. + """ + else: + env['SERVER_PORT'] = str(req.server.bind_addr[1]) + + # Request headers + env.update( + ('HTTP_' + bton(k).upper().replace('-', '_'), bton(v)) + for k, v in req.inheaders.items() + ) + + # CONTENT_TYPE/CONTENT_LENGTH + ct = env.pop('HTTP_CONTENT_TYPE', None) + if ct is not None: + env['CONTENT_TYPE'] = ct + cl = env.pop('HTTP_CONTENT_LENGTH', None) + if cl is not None: + env['CONTENT_LENGTH'] = cl + + if req.conn.ssl_env: + env.update(req.conn.ssl_env) + + return env + + +class Gateway_u0(Gateway_10): + """A Gateway class to interface HTTPServer with WSGI u.0. + + WSGI u.0 is an experimental protocol, which uses unicode for keys + and values in both Python 2 and Python 3. + """ + + version = 'u', 0 + + def get_environ(self): + """Return a new environ dict targeting the given wsgi.version.""" + req = self.req + env_10 = super(Gateway_u0, self).get_environ() + env = dict(map(self._decode_key, env_10.items())) + + # Request-URI + enc = env.setdefault(six.u('wsgi.url_encoding'), six.u('utf-8')) + try: + env['PATH_INFO'] = req.path.decode(enc) + env['QUERY_STRING'] = req.qs.decode(enc) + except UnicodeDecodeError: + # Fall back to latin 1 so apps can transcode if needed. + env['wsgi.url_encoding'] = 'ISO-8859-1' + env['PATH_INFO'] = env_10['PATH_INFO'] + env['QUERY_STRING'] = env_10['QUERY_STRING'] + + env.update(map(self._decode_value, env.items())) + + return env + + @staticmethod + def _decode_key(item): + k, v = item + if six.PY2: + k = k.decode('ISO-8859-1') + return k, v + + @staticmethod + def _decode_value(item): + k, v = item + skip_keys = 'REQUEST_URI', 'wsgi.input' + if six.PY3 or not isinstance(v, bytes) or k in skip_keys: + return k, v + return k, v.decode('ISO-8859-1') + + +wsgi_gateways = Gateway.gateway_map() + + +class PathInfoDispatcher: + """A WSGI dispatcher for dispatch based on the PATH_INFO.""" + + def __init__(self, apps): + """Initialize path info WSGI app dispatcher. + + Args: + apps (dict[str,object]|list[tuple[str,object]]): URI prefix + and WSGI app pairs + """ + try: + apps = list(apps.items()) + except AttributeError: + pass + + # Sort the apps by len(path), descending + def by_path_len(app): + return len(app[0]) + apps.sort(key=by_path_len, reverse=True) + + # The path_prefix strings must start, but not end, with a slash. + # Use "" instead of "/". + self.apps = [(p.rstrip('/'), a) for p, a in apps] + + def __call__(self, environ, start_response): + """Process incoming WSGI request. + + Ref: :pep:`3333` + + Args: + environ (Mapping): a dict containing WSGI environment variables + start_response (callable): function, which sets response + status and headers + + Returns: + list[bytes]: iterable containing bytes to be returned in + HTTP response body + + """ + path = environ['PATH_INFO'] or '/' + for p, app in self.apps: + # The apps list should be sorted by length, descending. + if path.startswith(p + '/') or path == p: + environ = environ.copy() + environ['SCRIPT_NAME'] = environ['SCRIPT_NAME'] + p + environ['PATH_INFO'] = path[len(p):] + return app(environ, start_response) + + start_response('404 Not Found', [('Content-Type', 'text/plain'), + ('Content-Length', '0')]) + return [''] + + +# compatibility aliases +globals().update( + WSGIServer=Server, + WSGIGateway=Gateway, + WSGIGateway_u0=Gateway_u0, + WSGIGateway_10=Gateway_10, + WSGIPathInfoDispatcher=PathInfoDispatcher, +) diff --git a/resources/lib/cherrypy/__init__.py b/resources/lib/cherrypy/__init__.py new file mode 100644 index 0000000..8e27c81 --- /dev/null +++ b/resources/lib/cherrypy/__init__.py @@ -0,0 +1,370 @@ +"""CherryPy is a pythonic, object-oriented HTTP framework. + +CherryPy consists of not one, but four separate API layers. + +The APPLICATION LAYER is the simplest. CherryPy applications are written as +a tree of classes and methods, where each branch in the tree corresponds to +a branch in the URL path. Each method is a 'page handler', which receives +GET and POST params as keyword arguments, and returns or yields the (HTML) +body of the response. The special method name 'index' is used for paths +that end in a slash, and the special method name 'default' is used to +handle multiple paths via a single handler. This layer also includes: + + * the 'exposed' attribute (and cherrypy.expose) + * cherrypy.quickstart() + * _cp_config attributes + * cherrypy.tools (including cherrypy.session) + * cherrypy.url() + +The ENVIRONMENT LAYER is used by developers at all levels. It provides +information about the current request and response, plus the application +and server environment, via a (default) set of top-level objects: + + * cherrypy.request + * cherrypy.response + * cherrypy.engine + * cherrypy.server + * cherrypy.tree + * cherrypy.config + * cherrypy.thread_data + * cherrypy.log + * cherrypy.HTTPError, NotFound, and HTTPRedirect + * cherrypy.lib + +The EXTENSION LAYER allows advanced users to construct and share their own +plugins. It consists of: + + * Hook API + * Tool API + * Toolbox API + * Dispatch API + * Config Namespace API + +Finally, there is the CORE LAYER, which uses the core API's to construct +the default components which are available at higher layers. You can think +of the default components as the 'reference implementation' for CherryPy. +Megaframeworks (and advanced users) may replace the default components +with customized or extended components. The core API's are: + + * Application API + * Engine API + * Request API + * Server API + * WSGI API + +These API's are described in the `CherryPy specification +`_. +""" + +try: + import pkg_resources +except ImportError: + pass + +from threading import local as _local + +from ._cperror import ( + HTTPError, HTTPRedirect, InternalRedirect, + NotFound, CherryPyException, +) + +from . import _cpdispatch as dispatch + +from ._cptools import default_toolbox as tools, Tool +from ._helper import expose, popargs, url + +from . import _cprequest, _cpserver, _cptree, _cplogging, _cpconfig + +import cherrypy.lib.httputil as _httputil + +from ._cptree import Application +from . import _cpwsgi as wsgi + +from . import process +try: + from .process import win32 + engine = win32.Win32Bus() + engine.console_control_handler = win32.ConsoleCtrlHandler(engine) + del win32 +except ImportError: + engine = process.bus + +from . import _cpchecker + +__all__ = ( + 'HTTPError', 'HTTPRedirect', 'InternalRedirect', + 'NotFound', 'CherryPyException', + 'dispatch', 'tools', 'Tool', 'Application', + 'wsgi', 'process', 'tree', 'engine', + 'quickstart', 'serving', 'request', 'response', 'thread_data', + 'log', 'expose', 'popargs', 'url', 'config', +) + + +__import__('cherrypy._cptools') +__import__('cherrypy._cprequest') + + +tree = _cptree.Tree() + + +try: + __version__ = pkg_resources.require('cherrypy')[0].version +except Exception: + __version__ = 'unknown' + + +engine.listeners['before_request'] = set() +engine.listeners['after_request'] = set() + + +engine.autoreload = process.plugins.Autoreloader(engine) +engine.autoreload.subscribe() + +engine.thread_manager = process.plugins.ThreadManager(engine) +engine.thread_manager.subscribe() + +engine.signal_handler = process.plugins.SignalHandler(engine) + + +class _HandleSignalsPlugin(object): + """Handle signals from other processes. + + Based on the configured platform handlers above. + """ + + def __init__(self, bus): + self.bus = bus + + def subscribe(self): + """Add the handlers based on the platform.""" + if hasattr(self.bus, 'signal_handler'): + self.bus.signal_handler.subscribe() + if hasattr(self.bus, 'console_control_handler'): + self.bus.console_control_handler.subscribe() + + +engine.signals = _HandleSignalsPlugin(engine) + + +server = _cpserver.Server() +server.subscribe() + + +def quickstart(root=None, script_name='', config=None): + """Mount the given root, start the builtin server (and engine), then block. + + root: an instance of a "controller class" (a collection of page handler + methods) which represents the root of the application. + script_name: a string containing the "mount point" of the application. + This should start with a slash, and be the path portion of the URL + at which to mount the given root. For example, if root.index() will + handle requests to "http://www.example.com:8080/dept/app1/", then + the script_name argument would be "/dept/app1". + + It MUST NOT end in a slash. If the script_name refers to the root + of the URI, it MUST be an empty string (not "/"). + config: a file or dict containing application config. If this contains + a [global] section, those entries will be used in the global + (site-wide) config. + """ + if config: + _global_conf_alias.update(config) + + tree.mount(root, script_name, config) + + engine.signals.subscribe() + engine.start() + engine.block() + + +class _Serving(_local): + """An interface for registering request and response objects. + + Rather than have a separate "thread local" object for the request and + the response, this class works as a single threadlocal container for + both objects (and any others which developers wish to define). In this + way, we can easily dump those objects when we stop/start a new HTTP + conversation, yet still refer to them as module-level globals in a + thread-safe way. + """ + + request = _cprequest.Request(_httputil.Host('127.0.0.1', 80), + _httputil.Host('127.0.0.1', 1111)) + """ + The request object for the current thread. In the main thread, + and any threads which are not receiving HTTP requests, this is None.""" + + response = _cprequest.Response() + """ + The response object for the current thread. In the main thread, + and any threads which are not receiving HTTP requests, this is None.""" + + def load(self, request, response): + self.request = request + self.response = response + + def clear(self): + """Remove all attributes of self.""" + self.__dict__.clear() + + +serving = _Serving() + + +class _ThreadLocalProxy(object): + + __slots__ = ['__attrname__', '__dict__'] + + def __init__(self, attrname): + self.__attrname__ = attrname + + def __getattr__(self, name): + child = getattr(serving, self.__attrname__) + return getattr(child, name) + + def __setattr__(self, name, value): + if name in ('__attrname__', ): + object.__setattr__(self, name, value) + else: + child = getattr(serving, self.__attrname__) + setattr(child, name, value) + + def __delattr__(self, name): + child = getattr(serving, self.__attrname__) + delattr(child, name) + + @property + def __dict__(self): + child = getattr(serving, self.__attrname__) + d = child.__class__.__dict__.copy() + d.update(child.__dict__) + return d + + def __getitem__(self, key): + child = getattr(serving, self.__attrname__) + return child[key] + + def __setitem__(self, key, value): + child = getattr(serving, self.__attrname__) + child[key] = value + + def __delitem__(self, key): + child = getattr(serving, self.__attrname__) + del child[key] + + def __contains__(self, key): + child = getattr(serving, self.__attrname__) + return key in child + + def __len__(self): + child = getattr(serving, self.__attrname__) + return len(child) + + def __nonzero__(self): + child = getattr(serving, self.__attrname__) + return bool(child) + # Python 3 + __bool__ = __nonzero__ + + +# Create request and response object (the same objects will be used +# throughout the entire life of the webserver, but will redirect +# to the "serving" object) +request = _ThreadLocalProxy('request') +response = _ThreadLocalProxy('response') + +# Create thread_data object as a thread-specific all-purpose storage + + +class _ThreadData(_local): + """A container for thread-specific data.""" + + +thread_data = _ThreadData() + + +# Monkeypatch pydoc to allow help() to go through the threadlocal proxy. +# Jan 2007: no Googleable examples of anyone else replacing pydoc.resolve. +# The only other way would be to change what is returned from type(request) +# and that's not possible in pure Python (you'd have to fake ob_type). +def _cherrypy_pydoc_resolve(thing, forceload=0): + """Given an object or a path to an object, get the object and its name.""" + if isinstance(thing, _ThreadLocalProxy): + thing = getattr(serving, thing.__attrname__) + return _pydoc._builtin_resolve(thing, forceload) + + +try: + import pydoc as _pydoc + _pydoc._builtin_resolve = _pydoc.resolve + _pydoc.resolve = _cherrypy_pydoc_resolve +except ImportError: + pass + + +class _GlobalLogManager(_cplogging.LogManager): + """A site-wide LogManager; routes to app.log or global log as appropriate. + + This :class:`LogManager` implements + cherrypy.log() and cherrypy.log.access(). If either + function is called during a request, the message will be sent to the + logger for the current Application. If they are called outside of a + request, the message will be sent to the site-wide logger. + """ + + def __call__(self, *args, **kwargs): + """Log the given message to the app.log or global log. + + Log the given message to the app.log or global + log as appropriate. + """ + # Do NOT use try/except here. See + # https://github.com/cherrypy/cherrypy/issues/945 + if hasattr(request, 'app') and hasattr(request.app, 'log'): + log = request.app.log + else: + log = self + return log.error(*args, **kwargs) + + def access(self): + """Log an access message to the app.log or global log. + + Log the given message to the app.log or global + log as appropriate. + """ + try: + return request.app.log.access() + except AttributeError: + return _cplogging.LogManager.access(self) + + +log = _GlobalLogManager() +# Set a default screen handler on the global log. +log.screen = True +log.error_file = '' +# Using an access file makes CP about 10% slower. Leave off by default. +log.access_file = '' + + +@engine.subscribe('log') +def _buslog(msg, level): + log.error(msg, 'ENGINE', severity=level) + + +# Use _global_conf_alias so quickstart can use 'config' as an arg +# without shadowing cherrypy.config. +config = _global_conf_alias = _cpconfig.Config() +config.defaults = { + 'tools.log_tracebacks.on': True, + 'tools.log_headers.on': True, + 'tools.trailing_slash.on': True, + 'tools.encode.on': True +} +config.namespaces['log'] = lambda k, v: setattr(log, k, v) +config.namespaces['checker'] = lambda k, v: setattr(checker, k, v) +# Must reset to get our defaults applied. +config.reset() + +checker = _cpchecker.Checker() +engine.subscribe('start', checker) diff --git a/resources/lib/cherrypy/__main__.py b/resources/lib/cherrypy/__main__.py new file mode 100644 index 0000000..6674f7c --- /dev/null +++ b/resources/lib/cherrypy/__main__.py @@ -0,0 +1,5 @@ +"""CherryPy'd cherryd daemon runner.""" +from cherrypy.daemon import run + + +__name__ == '__main__' and run() diff --git a/resources/lib/cherrypy/__pycache__/__init__.cpython-37.opt-1.pyc b/resources/lib/cherrypy/__pycache__/__init__.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6f9316f218cdd6f920ef361e4bca8aa63166658f GIT binary patch literal 11421 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 literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/__pycache__/_cpchecker.cpython-37.opt-1.pyc b/resources/lib/cherrypy/__pycache__/_cpchecker.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dde88f45f6bbc7f1c8c93e0a89cbb5e65e06c211 GIT binary patch literal 10406 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 literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/__pycache__/_cpconfig.cpython-37.opt-1.pyc b/resources/lib/cherrypy/__pycache__/_cpconfig.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b0c97fa7eb6fb665041fc2cca0b64ca8e35b1cf5 GIT binary patch literal 8919 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 literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/__pycache__/_cpdispatch.cpython-37.opt-1.pyc b/resources/lib/cherrypy/__pycache__/_cpdispatch.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..38761e9b956174ceb334d517dc948d93a044a9cc GIT binary patch literal 18276 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 U$_1X?!tAdaZ_KXGhSjP63sX_5i2wiq literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/__pycache__/_cperror.cpython-37.opt-1.pyc b/resources/lib/cherrypy/__pycache__/_cperror.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d6481aa242c06153402e0ca76f79cd25143b9c20 GIT binary patch literal 18080 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 literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/__pycache__/_cplogging.cpython-37.opt-1.pyc b/resources/lib/cherrypy/__pycache__/_cplogging.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e73c2afcdc6d2169f746b46a3fa1920473d1cff9 GIT binary patch literal 15137 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 literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/__pycache__/_cpnative_server.cpython-37.opt-1.pyc b/resources/lib/cherrypy/__pycache__/_cpnative_server.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0908f716c244586f1938051ee67c007ccb05160 GIT binary patch literal 4335 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 B!kz#C literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/__pycache__/_cpreqbody.cpython-37.opt-1.pyc b/resources/lib/cherrypy/__pycache__/_cpreqbody.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7a9078aff5462bdf43b110c9109cc7da6f600ee4 GIT binary patch literal 23141 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 literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/__pycache__/_cprequest.cpython-37.opt-1.pyc b/resources/lib/cherrypy/__pycache__/_cprequest.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a750d8004ed0a5b6be451e7d8bdfb4177fba5b57 GIT binary patch literal 16272 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 literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/__pycache__/_cptools.cpython-37.opt-1.pyc b/resources/lib/cherrypy/__pycache__/_cptools.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5183d60ac628fe56cbbb4b53dd2fe00847f156ca GIT binary patch literal 17541 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 literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/__pycache__/_cptree.cpython-37.opt-1.pyc b/resources/lib/cherrypy/__pycache__/_cptree.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d64e4dc93a54c6bb71be377ddd29714661256988 GIT binary patch literal 9286 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 literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/__pycache__/_cpwsgi.cpython-37.opt-1.pyc b/resources/lib/cherrypy/__pycache__/_cpwsgi.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..13e0e21ed1bcd32132f0662fcdbf2b7498f9d515 GIT binary patch literal 11479 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 literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/__pycache__/_helper.cpython-37.opt-1.pyc b/resources/lib/cherrypy/__pycache__/_helper.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..02386a4d646ba073e610ebb22f22c50a6d2077a2 GIT binary patch literal 8755 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 1: + if atoms[0] not in ns: + # Spit out a special warning if a known + # namespace is preceded by "cherrypy." + if atoms[0] == 'cherrypy' and atoms[1] in ns: + msg = ( + 'The config entry %r is invalid; ' + 'try %r instead.\nsection: [%s]' + % (k, '.'.join(atoms[1:]), section)) + else: + msg = ( + 'The config entry %r is invalid, ' + 'because the %r config namespace ' + 'is unknown.\n' + 'section: [%s]' % (k, atoms[0], section)) + warnings.warn(msg) + elif atoms[0] == 'tools': + if atoms[1] not in dir(cherrypy.tools): + msg = ( + 'The config entry %r may be invalid, ' + 'because the %r tool was not found.\n' + 'section: [%s]' % (k, atoms[1], section)) + warnings.warn(msg) + + def check_config_namespaces(self): + """Process config and warn on each unknown config namespace.""" + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + self._known_ns(app) + + # -------------------------- Config Types -------------------------- # + known_config_types = {} + + def _populate_known_types(self): + b = [x for x in vars(builtins).values() + if type(x) is type(str)] + + def traverse(obj, namespace): + for name in dir(obj): + # Hack for 3.2's warning about body_params + if name == 'body_params': + continue + vtype = type(getattr(obj, name, None)) + if vtype in b: + self.known_config_types[namespace + '.' + name] = vtype + + traverse(cherrypy.request, 'request') + traverse(cherrypy.response, 'response') + traverse(cherrypy.server, 'server') + traverse(cherrypy.engine, 'engine') + traverse(cherrypy.log, 'log') + + def _known_types(self, config): + msg = ('The config entry %r in section %r is of type %r, ' + 'which does not match the expected type %r.') + + for section, conf in config.items(): + if not isinstance(conf, dict): + conf = {section: conf} + for k, v in conf.items(): + if v is not None: + expected_type = self.known_config_types.get(k, None) + vtype = type(v) + if expected_type and vtype != expected_type: + warnings.warn(msg % (k, section, vtype.__name__, + expected_type.__name__)) + + def check_config_types(self): + """Assert that config values are of the same type as default values.""" + self._known_types(cherrypy.config) + for sn, app in cherrypy.tree.apps.items(): + if not isinstance(app, cherrypy.Application): + continue + self._known_types(app.config) + + # -------------------- Specific config warnings -------------------- # + def check_localhost(self): + """Warn if any socket_host is 'localhost'. See #711.""" + for k, v in cherrypy.config.items(): + if k == 'server.socket_host' and v == 'localhost': + warnings.warn("The use of 'localhost' as a socket host can " + 'cause problems on newer systems, since ' + "'localhost' can map to either an IPv4 or an " + "IPv6 address. You should use '127.0.0.1' " + "or '[::1]' instead.") diff --git a/resources/lib/cherrypy/_cpcompat.py b/resources/lib/cherrypy/_cpcompat.py new file mode 100644 index 0000000..f454505 --- /dev/null +++ b/resources/lib/cherrypy/_cpcompat.py @@ -0,0 +1,162 @@ +"""Compatibility code for using CherryPy with various versions of Python. + +To retain compatibility with older Python versions, this module provides a +useful abstraction over the differences between Python versions, sometimes by +preferring a newer idiom, sometimes an older one, and sometimes a custom one. + +In particular, Python 2 uses str and '' for byte strings, while Python 3 +uses str and '' for unicode strings. We will call each of these the 'native +string' type for each version. Because of this major difference, this module +provides +two functions: 'ntob', which translates native strings (of type 'str') into +byte strings regardless of Python version, and 'ntou', which translates native +strings to unicode strings. + +Try not to use the compatibility functions 'ntob', 'ntou', 'tonative'. +They were created with Python 2.3-2.5 compatibility in mind. +Instead, use unicode literals (from __future__) and bytes literals +and their .encode/.decode methods as needed. +""" + +import re +import sys +import threading + +import six +from six.moves import urllib + + +if six.PY3: + def ntob(n, encoding='ISO-8859-1'): + """Return the given native string as a byte string in the given + encoding. + """ + assert_native(n) + # In Python 3, the native string type is unicode + return n.encode(encoding) + + def ntou(n, encoding='ISO-8859-1'): + """Return the given native string as a unicode string with the given + encoding. + """ + assert_native(n) + # In Python 3, the native string type is unicode + return n + + def tonative(n, encoding='ISO-8859-1'): + """Return the given string as a native string in the given encoding.""" + # In Python 3, the native string type is unicode + if isinstance(n, bytes): + return n.decode(encoding) + return n +else: + # Python 2 + def ntob(n, encoding='ISO-8859-1'): + """Return the given native string as a byte string in the given + encoding. + """ + assert_native(n) + # In Python 2, the native string type is bytes. Assume it's already + # in the given encoding, which for ISO-8859-1 is almost always what + # was intended. + return n + + def ntou(n, encoding='ISO-8859-1'): + """Return the given native string as a unicode string with the given + encoding. + """ + assert_native(n) + # In Python 2, the native string type is bytes. + # First, check for the special encoding 'escape'. The test suite uses + # this to signal that it wants to pass a string with embedded \uXXXX + # escapes, but without having to prefix it with u'' for Python 2, + # but no prefix for Python 3. + if encoding == 'escape': + return six.text_type( # unicode for Python 2 + re.sub(r'\\u([0-9a-zA-Z]{4})', + lambda m: six.unichr(int(m.group(1), 16)), + n.decode('ISO-8859-1'))) + # Assume it's already in the given encoding, which for ISO-8859-1 + # is almost always what was intended. + return n.decode(encoding) + + def tonative(n, encoding='ISO-8859-1'): + """Return the given string as a native string in the given encoding.""" + # In Python 2, the native string type is bytes. + if isinstance(n, six.text_type): # unicode for Python 2 + return n.encode(encoding) + return n + + +def assert_native(n): + if not isinstance(n, str): + raise TypeError('n must be a native str (got %s)' % type(n).__name__) + + +# Some platforms don't expose HTTPSConnection, so handle it separately +HTTPSConnection = getattr(six.moves.http_client, 'HTTPSConnection', None) + + +def _unquote_plus_compat(string, encoding='utf-8', errors='replace'): + return urllib.parse.unquote_plus(string).decode(encoding, errors) + + +def _unquote_compat(string, encoding='utf-8', errors='replace'): + return urllib.parse.unquote(string).decode(encoding, errors) + + +def _quote_compat(string, encoding='utf-8', errors='replace'): + return urllib.parse.quote(string.encode(encoding, errors)) + + +unquote_plus = urllib.parse.unquote_plus if six.PY3 else _unquote_plus_compat +unquote = urllib.parse.unquote if six.PY3 else _unquote_compat +quote = urllib.parse.quote if six.PY3 else _quote_compat + +try: + # Prefer simplejson + import simplejson as json +except ImportError: + import json + + +json_decode = json.JSONDecoder().decode +_json_encode = json.JSONEncoder().iterencode + + +if six.PY3: + # Encode to bytes on Python 3 + def json_encode(value): + for chunk in _json_encode(value): + yield chunk.encode('utf-8') +else: + json_encode = _json_encode + + +text_or_bytes = six.text_type, bytes + + +if sys.version_info >= (3, 3): + Timer = threading.Timer + Event = threading.Event +else: + # Python 3.2 and earlier + Timer = threading._Timer + Event = threading._Event + +# html module come in 3.2 version +try: + from html import escape +except ImportError: + from cgi import escape + + +# html module needed the argument quote=False because in cgi the default +# is False. With quote=True the results differ. + +def escape_html(s, escape_quote=False): + """Replace special characters "&", "<" and ">" to HTML-safe sequences. + + When escape_quote=True, escape (') and (") chars. + """ + return escape(s, quote=escape_quote) diff --git a/resources/lib/cherrypy/_cpconfig.py b/resources/lib/cherrypy/_cpconfig.py new file mode 100644 index 0000000..8e3fd61 --- /dev/null +++ b/resources/lib/cherrypy/_cpconfig.py @@ -0,0 +1,296 @@ +""" +Configuration system for CherryPy. + +Configuration in CherryPy is implemented via dictionaries. Keys are strings +which name the mapped value, which may be of any type. + + +Architecture +------------ + +CherryPy Requests are part of an Application, which runs in a global context, +and configuration data may apply to any of those three scopes: + +Global + Configuration entries which apply everywhere are stored in + cherrypy.config. + +Application + Entries which apply to each mounted application are stored + on the Application object itself, as 'app.config'. This is a two-level + dict where each key is a path, or "relative URL" (for example, "/" or + "/path/to/my/page"), and each value is a config dict. Usually, this + data is provided in the call to tree.mount(root(), config=conf), + although you may also use app.merge(conf). + +Request + Each Request object possesses a single 'Request.config' dict. + Early in the request process, this dict is populated by merging global + config entries, Application entries (whose path equals or is a parent + of Request.path_info), and any config acquired while looking up the + page handler (see next). + + +Declaration +----------- + +Configuration data may be supplied as a Python dictionary, as a filename, +or as an open file object. When you supply a filename or file, CherryPy +uses Python's builtin ConfigParser; you declare Application config by +writing each path as a section header:: + + [/path/to/my/page] + request.stream = True + +To declare global configuration entries, place them in a [global] section. + +You may also declare config entries directly on the classes and methods +(page handlers) that make up your CherryPy application via the ``_cp_config`` +attribute, set with the ``cherrypy.config`` decorator. For example:: + + @cherrypy.config(**{'tools.gzip.on': True}) + class Demo: + + @cherrypy.expose + @cherrypy.config(**{'request.show_tracebacks': False}) + def index(self): + return "Hello world" + +.. note:: + + This behavior is only guaranteed for the default dispatcher. + Other dispatchers may have different restrictions on where + you can attach config attributes. + + +Namespaces +---------- + +Configuration keys are separated into namespaces by the first "." in the key. +Current namespaces: + +engine + Controls the 'application engine', including autoreload. + These can only be declared in the global config. + +tree + Grafts cherrypy.Application objects onto cherrypy.tree. + These can only be declared in the global config. + +hooks + Declares additional request-processing functions. + +log + Configures the logging for each application. + These can only be declared in the global or / config. + +request + Adds attributes to each Request. + +response + Adds attributes to each Response. + +server + Controls the default HTTP server via cherrypy.server. + These can only be declared in the global config. + +tools + Runs and configures additional request-processing packages. + +wsgi + Adds WSGI middleware to an Application's "pipeline". + These can only be declared in the app's root config ("/"). + +checker + Controls the 'checker', which looks for common errors in + app state (including config) when the engine starts. + Global config only. + +The only key that does not exist in a namespace is the "environment" entry. +This special entry 'imports' other config entries from a template stored in +cherrypy._cpconfig.environments[environment]. It only applies to the global +config, and only when you use cherrypy.config.update. + +You can define your own namespaces to be called at the Global, Application, +or Request level, by adding a named handler to cherrypy.config.namespaces, +app.namespaces, or app.request_class.namespaces. The name can +be any string, and the handler must be either a callable or a (Python 2.5 +style) context manager. +""" + +import cherrypy +from cherrypy._cpcompat import text_or_bytes +from cherrypy.lib import reprconf + + +def _if_filename_register_autoreload(ob): + """Register for autoreload if ob is a string (presumed filename).""" + is_filename = isinstance(ob, text_or_bytes) + is_filename and cherrypy.engine.autoreload.files.add(ob) + + +def merge(base, other): + """Merge one app config (from a dict, file, or filename) into another. + + If the given config is a filename, it will be appended to + the list of files to monitor for "autoreload" changes. + """ + _if_filename_register_autoreload(other) + + # Load other into base + for section, value_map in reprconf.Parser.load(other).items(): + if not isinstance(value_map, dict): + raise ValueError( + 'Application config must include section headers, but the ' + "config you tried to merge doesn't have any sections. " + 'Wrap your config in another dict with paths as section ' + "headers, for example: {'/': config}.") + base.setdefault(section, {}).update(value_map) + + +class Config(reprconf.Config): + """The 'global' configuration data for the entire CherryPy process.""" + + def update(self, config): + """Update self from a dict, file or filename.""" + _if_filename_register_autoreload(config) + super(Config, self).update(config) + + def _apply(self, config): + """Update self from a dict.""" + if isinstance(config.get('global'), dict): + if len(config) > 1: + cherrypy.checker.global_config_contained_paths = True + config = config['global'] + if 'tools.staticdir.dir' in config: + config['tools.staticdir.section'] = 'global' + super(Config, self)._apply(config) + + @staticmethod + def __call__(**kwargs): + """Decorate for page handlers to set _cp_config.""" + def tool_decorator(f): + _Vars(f).setdefault('_cp_config', {}).update(kwargs) + return f + return tool_decorator + + +class _Vars(object): + """Adapter allowing setting a default attribute on a function or class.""" + + def __init__(self, target): + self.target = target + + def setdefault(self, key, default): + if not hasattr(self.target, key): + setattr(self.target, key, default) + return getattr(self.target, key) + + +# Sphinx begin config.environments +Config.environments = environments = { + 'staging': { + 'engine.autoreload.on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': False, + 'request.show_mismatched_params': False, + }, + 'production': { + 'engine.autoreload.on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': False, + 'request.show_mismatched_params': False, + 'log.screen': False, + }, + 'embedded': { + # For use with CherryPy embedded in another deployment stack. + 'engine.autoreload.on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': False, + 'request.show_mismatched_params': False, + 'log.screen': False, + 'engine.SIGHUP': None, + 'engine.SIGTERM': None, + }, + 'test_suite': { + 'engine.autoreload.on': False, + 'checker.on': False, + 'tools.log_headers.on': False, + 'request.show_tracebacks': True, + 'request.show_mismatched_params': True, + 'log.screen': False, + }, +} +# Sphinx end config.environments + + +def _server_namespace_handler(k, v): + """Config handler for the "server" namespace.""" + atoms = k.split('.', 1) + if len(atoms) > 1: + # Special-case config keys of the form 'server.servername.socket_port' + # to configure additional HTTP servers. + if not hasattr(cherrypy, 'servers'): + cherrypy.servers = {} + + servername, k = atoms + if servername not in cherrypy.servers: + from cherrypy import _cpserver + cherrypy.servers[servername] = _cpserver.Server() + # On by default, but 'on = False' can unsubscribe it (see below). + cherrypy.servers[servername].subscribe() + + if k == 'on': + if v: + cherrypy.servers[servername].subscribe() + else: + cherrypy.servers[servername].unsubscribe() + else: + setattr(cherrypy.servers[servername], k, v) + else: + setattr(cherrypy.server, k, v) + + +Config.namespaces['server'] = _server_namespace_handler + + +def _engine_namespace_handler(k, v): + """Config handler for the "engine" namespace.""" + engine = cherrypy.engine + + if k in {'SIGHUP', 'SIGTERM'}: + engine.subscribe(k, v) + return + + if '.' in k: + plugin, attrname = k.split('.', 1) + plugin = getattr(engine, plugin) + op = 'subscribe' if v else 'unsubscribe' + sub_unsub = getattr(plugin, op, None) + if attrname == 'on' and callable(sub_unsub): + sub_unsub() + return + setattr(plugin, attrname, v) + else: + setattr(engine, k, v) + + +Config.namespaces['engine'] = _engine_namespace_handler + + +def _tree_namespace_handler(k, v): + """Namespace handler for the 'tree' config namespace.""" + if isinstance(v, dict): + for script_name, app in v.items(): + cherrypy.tree.graft(app, script_name) + msg = 'Mounted: %s on %s' % (app, script_name or '/') + cherrypy.engine.log(msg) + else: + cherrypy.tree.graft(v, v.script_name) + cherrypy.engine.log('Mounted: %s on %s' % (v, v.script_name or '/')) + + +Config.namespaces['tree'] = _tree_namespace_handler diff --git a/resources/lib/cherrypy/_cpdispatch.py b/resources/lib/cherrypy/_cpdispatch.py new file mode 100644 index 0000000..83eb79c --- /dev/null +++ b/resources/lib/cherrypy/_cpdispatch.py @@ -0,0 +1,686 @@ +"""CherryPy dispatchers. + +A 'dispatcher' is the object which looks up the 'page handler' callable +and collects config for the current request based on the path_info, other +request attributes, and the application architecture. The core calls the +dispatcher as early as possible, passing it a 'path_info' argument. + +The default dispatcher discovers the page handler by matching path_info +to a hierarchical arrangement of objects, starting at request.app.root. +""" + +import string +import sys +import types +try: + classtype = (type, types.ClassType) +except AttributeError: + classtype = type + +import cherrypy + + +class PageHandler(object): + + """Callable which sets response.body.""" + + def __init__(self, callable, *args, **kwargs): + self.callable = callable + self.args = args + self.kwargs = kwargs + + @property + def args(self): + """The ordered args should be accessible from post dispatch hooks.""" + return cherrypy.serving.request.args + + @args.setter + def args(self, args): + cherrypy.serving.request.args = args + return cherrypy.serving.request.args + + @property + def kwargs(self): + """The named kwargs should be accessible from post dispatch hooks.""" + return cherrypy.serving.request.kwargs + + @kwargs.setter + def kwargs(self, kwargs): + cherrypy.serving.request.kwargs = kwargs + return cherrypy.serving.request.kwargs + + def __call__(self): + try: + return self.callable(*self.args, **self.kwargs) + except TypeError: + x = sys.exc_info()[1] + try: + test_callable_spec(self.callable, self.args, self.kwargs) + except cherrypy.HTTPError: + raise sys.exc_info()[1] + except Exception: + raise x + raise + + +def test_callable_spec(callable, callable_args, callable_kwargs): + """ + Inspect callable and test to see if the given args are suitable for it. + + When an error occurs during the handler's invoking stage there are 2 + erroneous cases: + 1. Too many parameters passed to a function which doesn't define + one of *args or **kwargs. + 2. Too little parameters are passed to the function. + + There are 3 sources of parameters to a cherrypy handler. + 1. query string parameters are passed as keyword parameters to the + handler. + 2. body parameters are also passed as keyword parameters. + 3. when partial matching occurs, the final path atoms are passed as + positional args. + Both the query string and path atoms are part of the URI. If they are + incorrect, then a 404 Not Found should be raised. Conversely the body + parameters are part of the request; if they are invalid a 400 Bad Request. + """ + show_mismatched_params = getattr( + cherrypy.serving.request, 'show_mismatched_params', False) + try: + (args, varargs, varkw, defaults) = getargspec(callable) + except TypeError: + if isinstance(callable, object) and hasattr(callable, '__call__'): + (args, varargs, varkw, + defaults) = getargspec(callable.__call__) + else: + # If it wasn't one of our own types, re-raise + # the original error + raise + + if args and ( + # For callable objects, which have a __call__(self) method + hasattr(callable, '__call__') or + # For normal methods + inspect.ismethod(callable) + ): + # Strip 'self' + args = args[1:] + + arg_usage = dict([(arg, 0,) for arg in args]) + vararg_usage = 0 + varkw_usage = 0 + extra_kwargs = set() + + for i, value in enumerate(callable_args): + try: + arg_usage[args[i]] += 1 + except IndexError: + vararg_usage += 1 + + for key in callable_kwargs.keys(): + try: + arg_usage[key] += 1 + except KeyError: + varkw_usage += 1 + extra_kwargs.add(key) + + # figure out which args have defaults. + args_with_defaults = args[-len(defaults or []):] + for i, val in enumerate(defaults or []): + # Defaults take effect only when the arg hasn't been used yet. + if arg_usage[args_with_defaults[i]] == 0: + arg_usage[args_with_defaults[i]] += 1 + + missing_args = [] + multiple_args = [] + for key, usage in arg_usage.items(): + if usage == 0: + missing_args.append(key) + elif usage > 1: + multiple_args.append(key) + + if missing_args: + # In the case where the method allows body arguments + # there are 3 potential errors: + # 1. not enough query string parameters -> 404 + # 2. not enough body parameters -> 400 + # 3. not enough path parts (partial matches) -> 404 + # + # We can't actually tell which case it is, + # so I'm raising a 404 because that covers 2/3 of the + # possibilities + # + # In the case where the method does not allow body + # arguments it's definitely a 404. + message = None + if show_mismatched_params: + message = 'Missing parameters: %s' % ','.join(missing_args) + raise cherrypy.HTTPError(404, message=message) + + # the extra positional arguments come from the path - 404 Not Found + if not varargs and vararg_usage > 0: + raise cherrypy.HTTPError(404) + + body_params = cherrypy.serving.request.body.params or {} + body_params = set(body_params.keys()) + qs_params = set(callable_kwargs.keys()) - body_params + + if multiple_args: + if qs_params.intersection(set(multiple_args)): + # If any of the multiple parameters came from the query string then + # it's a 404 Not Found + error = 404 + else: + # Otherwise it's a 400 Bad Request + error = 400 + + message = None + if show_mismatched_params: + message = 'Multiple values for parameters: '\ + '%s' % ','.join(multiple_args) + raise cherrypy.HTTPError(error, message=message) + + if not varkw and varkw_usage > 0: + + # If there were extra query string parameters, it's a 404 Not Found + extra_qs_params = set(qs_params).intersection(extra_kwargs) + if extra_qs_params: + message = None + if show_mismatched_params: + message = 'Unexpected query string '\ + 'parameters: %s' % ', '.join(extra_qs_params) + raise cherrypy.HTTPError(404, message=message) + + # If there were any extra body parameters, it's a 400 Not Found + extra_body_params = set(body_params).intersection(extra_kwargs) + if extra_body_params: + message = None + if show_mismatched_params: + message = 'Unexpected body parameters: '\ + '%s' % ', '.join(extra_body_params) + raise cherrypy.HTTPError(400, message=message) + + +try: + import inspect +except ImportError: + def test_callable_spec(callable, args, kwargs): # noqa: F811 + return None +else: + getargspec = inspect.getargspec + # Python 3 requires using getfullargspec if + # keyword-only arguments are present + if hasattr(inspect, 'getfullargspec'): + def getargspec(callable): + return inspect.getfullargspec(callable)[:4] + + +class LateParamPageHandler(PageHandler): + + """When passing cherrypy.request.params to the page handler, we do not + want to capture that dict too early; we want to give tools like the + decoding tool a chance to modify the params dict in-between the lookup + of the handler and the actual calling of the handler. This subclass + takes that into account, and allows request.params to be 'bound late' + (it's more complicated than that, but that's the effect). + """ + + @property + def kwargs(self): + """Page handler kwargs (with cherrypy.request.params copied in).""" + kwargs = cherrypy.serving.request.params.copy() + if self._kwargs: + kwargs.update(self._kwargs) + return kwargs + + @kwargs.setter + def kwargs(self, kwargs): + cherrypy.serving.request.kwargs = kwargs + self._kwargs = kwargs + + +if sys.version_info < (3, 0): + punctuation_to_underscores = string.maketrans( + string.punctuation, '_' * len(string.punctuation)) + + def validate_translator(t): + if not isinstance(t, str) or len(t) != 256: + raise ValueError( + 'The translate argument must be a str of len 256.') +else: + punctuation_to_underscores = str.maketrans( + string.punctuation, '_' * len(string.punctuation)) + + def validate_translator(t): + if not isinstance(t, dict): + raise ValueError('The translate argument must be a dict.') + + +class Dispatcher(object): + + """CherryPy Dispatcher which walks a tree of objects to find a handler. + + The tree is rooted at cherrypy.request.app.root, and each hierarchical + component in the path_info argument is matched to a corresponding nested + attribute of the root object. Matching handlers must have an 'exposed' + attribute which evaluates to True. The special method name "index" + matches a URI which ends in a slash ("/"). The special method name + "default" may match a portion of the path_info (but only when no longer + substring of the path_info matches some other object). + + This is the default, built-in dispatcher for CherryPy. + """ + + dispatch_method_name = '_cp_dispatch' + """ + The name of the dispatch method that nodes may optionally implement + to provide their own dynamic dispatch algorithm. + """ + + def __init__(self, dispatch_method_name=None, + translate=punctuation_to_underscores): + validate_translator(translate) + self.translate = translate + if dispatch_method_name: + self.dispatch_method_name = dispatch_method_name + + def __call__(self, path_info): + """Set handler and config for the current request.""" + request = cherrypy.serving.request + func, vpath = self.find_handler(path_info) + + if func: + # Decode any leftover %2F in the virtual_path atoms. + vpath = [x.replace('%2F', '/') for x in vpath] + request.handler = LateParamPageHandler(func, *vpath) + else: + request.handler = cherrypy.NotFound() + + def find_handler(self, path): + """Return the appropriate page handler, plus any virtual path. + + This will return two objects. The first will be a callable, + which can be used to generate page output. Any parameters from + the query string or request body will be sent to that callable + as keyword arguments. + + The callable is found by traversing the application's tree, + starting from cherrypy.request.app.root, and matching path + components to successive objects in the tree. For example, the + URL "/path/to/handler" might return root.path.to.handler. + + The second object returned will be a list of names which are + 'virtual path' components: parts of the URL which are dynamic, + and were not used when looking up the handler. + These virtual path components are passed to the handler as + positional arguments. + """ + request = cherrypy.serving.request + app = request.app + root = app.root + dispatch_name = self.dispatch_method_name + + # Get config for the root object/path. + fullpath = [x for x in path.strip('/').split('/') if x] + ['index'] + fullpath_len = len(fullpath) + segleft = fullpath_len + nodeconf = {} + if hasattr(root, '_cp_config'): + nodeconf.update(root._cp_config) + if '/' in app.config: + nodeconf.update(app.config['/']) + object_trail = [['root', root, nodeconf, segleft]] + + node = root + iternames = fullpath[:] + while iternames: + name = iternames[0] + # map to legal Python identifiers (e.g. replace '.' with '_') + objname = name.translate(self.translate) + + nodeconf = {} + subnode = getattr(node, objname, None) + pre_len = len(iternames) + if subnode is None: + dispatch = getattr(node, dispatch_name, None) + if dispatch and hasattr(dispatch, '__call__') and not \ + getattr(dispatch, 'exposed', False) and \ + pre_len > 1: + # Don't expose the hidden 'index' token to _cp_dispatch + # We skip this if pre_len == 1 since it makes no sense + # to call a dispatcher when we have no tokens left. + index_name = iternames.pop() + subnode = dispatch(vpath=iternames) + iternames.append(index_name) + else: + # We didn't find a path, but keep processing in case there + # is a default() handler. + iternames.pop(0) + else: + # We found the path, remove the vpath entry + iternames.pop(0) + segleft = len(iternames) + if segleft > pre_len: + # No path segment was removed. Raise an error. + raise cherrypy.CherryPyException( + 'A vpath segment was added. Custom dispatchers may only ' + 'remove elements. While trying to process ' + '{0} in {1}'.format(name, fullpath) + ) + elif segleft == pre_len: + # Assume that the handler used the current path segment, but + # did not pop it. This allows things like + # return getattr(self, vpath[0], None) + iternames.pop(0) + segleft -= 1 + node = subnode + + if node is not None: + # Get _cp_config attached to this node. + if hasattr(node, '_cp_config'): + nodeconf.update(node._cp_config) + + # Mix in values from app.config for this path. + existing_len = fullpath_len - pre_len + if existing_len != 0: + curpath = '/' + '/'.join(fullpath[0:existing_len]) + else: + curpath = '' + new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft] + for seg in new_segs: + curpath += '/' + seg + if curpath in app.config: + nodeconf.update(app.config[curpath]) + + object_trail.append([name, node, nodeconf, segleft]) + + def set_conf(): + """Collapse all object_trail config into cherrypy.request.config. + """ + base = cherrypy.config.copy() + # Note that we merge the config from each node + # even if that node was None. + for name, obj, conf, segleft in object_trail: + base.update(conf) + if 'tools.staticdir.dir' in conf: + base['tools.staticdir.section'] = '/' + \ + '/'.join(fullpath[0:fullpath_len - segleft]) + return base + + # Try successive objects (reverse order) + num_candidates = len(object_trail) - 1 + for i in range(num_candidates, -1, -1): + + name, candidate, nodeconf, segleft = object_trail[i] + if candidate is None: + continue + + # Try a "default" method on the current leaf. + if hasattr(candidate, 'default'): + defhandler = candidate.default + if getattr(defhandler, 'exposed', False): + # Insert any extra _cp_config from the default handler. + conf = getattr(defhandler, '_cp_config', {}) + object_trail.insert( + i + 1, ['default', defhandler, conf, segleft]) + request.config = set_conf() + # See https://github.com/cherrypy/cherrypy/issues/613 + request.is_index = path.endswith('/') + return defhandler, fullpath[fullpath_len - segleft:-1] + + # Uncomment the next line to restrict positional params to + # "default". + # if i < num_candidates - 2: continue + + # Try the current leaf. + if getattr(candidate, 'exposed', False): + request.config = set_conf() + if i == num_candidates: + # We found the extra ".index". Mark request so tools + # can redirect if path_info has no trailing slash. + request.is_index = True + else: + # We're not at an 'index' handler. Mark request so tools + # can redirect if path_info has NO trailing slash. + # Note that this also includes handlers which take + # positional parameters (virtual paths). + request.is_index = False + return candidate, fullpath[fullpath_len - segleft:-1] + + # We didn't find anything + request.config = set_conf() + return None, [] + + +class MethodDispatcher(Dispatcher): + + """Additional dispatch based on cherrypy.request.method.upper(). + + Methods named GET, POST, etc will be called on an exposed class. + The method names must be all caps; the appropriate Allow header + will be output showing all capitalized method names as allowable + HTTP verbs. + + Note that the containing class must be exposed, not the methods. + """ + + def __call__(self, path_info): + """Set handler and config for the current request.""" + request = cherrypy.serving.request + resource, vpath = self.find_handler(path_info) + + if resource: + # Set Allow header + avail = [m for m in dir(resource) if m.isupper()] + if 'GET' in avail and 'HEAD' not in avail: + avail.append('HEAD') + avail.sort() + cherrypy.serving.response.headers['Allow'] = ', '.join(avail) + + # Find the subhandler + meth = request.method.upper() + func = getattr(resource, meth, None) + if func is None and meth == 'HEAD': + func = getattr(resource, 'GET', None) + if func: + # Grab any _cp_config on the subhandler. + if hasattr(func, '_cp_config'): + request.config.update(func._cp_config) + + # Decode any leftover %2F in the virtual_path atoms. + vpath = [x.replace('%2F', '/') for x in vpath] + request.handler = LateParamPageHandler(func, *vpath) + else: + request.handler = cherrypy.HTTPError(405) + else: + request.handler = cherrypy.NotFound() + + +class RoutesDispatcher(object): + + """A Routes based dispatcher for CherryPy.""" + + def __init__(self, full_result=False, **mapper_options): + """ + Routes dispatcher + + Set full_result to True if you wish the controller + and the action to be passed on to the page handler + parameters. By default they won't be. + """ + import routes + self.full_result = full_result + self.controllers = {} + self.mapper = routes.Mapper(**mapper_options) + self.mapper.controller_scan = self.controllers.keys + + def connect(self, name, route, controller, **kwargs): + self.controllers[name] = controller + self.mapper.connect(name, route, controller=name, **kwargs) + + def redirect(self, url): + raise cherrypy.HTTPRedirect(url) + + def __call__(self, path_info): + """Set handler and config for the current request.""" + func = self.find_handler(path_info) + if func: + cherrypy.serving.request.handler = LateParamPageHandler(func) + else: + cherrypy.serving.request.handler = cherrypy.NotFound() + + def find_handler(self, path_info): + """Find the right page handler, and set request.config.""" + import routes + + request = cherrypy.serving.request + + config = routes.request_config() + config.mapper = self.mapper + if hasattr(request, 'wsgi_environ'): + config.environ = request.wsgi_environ + config.host = request.headers.get('Host', None) + config.protocol = request.scheme + config.redirect = self.redirect + + result = self.mapper.match(path_info) + + config.mapper_dict = result + params = {} + if result: + params = result.copy() + if not self.full_result: + params.pop('controller', None) + params.pop('action', None) + request.params.update(params) + + # Get config for the root object/path. + request.config = base = cherrypy.config.copy() + curpath = '' + + def merge(nodeconf): + if 'tools.staticdir.dir' in nodeconf: + nodeconf['tools.staticdir.section'] = curpath or '/' + base.update(nodeconf) + + app = request.app + root = app.root + if hasattr(root, '_cp_config'): + merge(root._cp_config) + if '/' in app.config: + merge(app.config['/']) + + # Mix in values from app.config. + atoms = [x for x in path_info.split('/') if x] + if atoms: + last = atoms.pop() + else: + last = None + for atom in atoms: + curpath = '/'.join((curpath, atom)) + if curpath in app.config: + merge(app.config[curpath]) + + handler = None + if result: + controller = result.get('controller') + controller = self.controllers.get(controller, controller) + if controller: + if isinstance(controller, classtype): + controller = controller() + # Get config from the controller. + if hasattr(controller, '_cp_config'): + merge(controller._cp_config) + + action = result.get('action') + if action is not None: + handler = getattr(controller, action, None) + # Get config from the handler + if hasattr(handler, '_cp_config'): + merge(handler._cp_config) + else: + handler = controller + + # Do the last path atom here so it can + # override the controller's _cp_config. + if last: + curpath = '/'.join((curpath, last)) + if curpath in app.config: + merge(app.config[curpath]) + + return handler + + +def XMLRPCDispatcher(next_dispatcher=Dispatcher()): + from cherrypy.lib import xmlrpcutil + + def xmlrpc_dispatch(path_info): + path_info = xmlrpcutil.patched_path(path_info) + return next_dispatcher(path_info) + return xmlrpc_dispatch + + +def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True, + **domains): + """ + Select a different handler based on the Host header. + + This can be useful when running multiple sites within one CP server. + It allows several domains to point to different parts of a single + website structure. For example:: + + http://www.domain.example -> root + http://www.domain2.example -> root/domain2/ + http://www.domain2.example:443 -> root/secure + + can be accomplished via the following config:: + + [/] + request.dispatch = cherrypy.dispatch.VirtualHost( + **{'www.domain2.example': '/domain2', + 'www.domain2.example:443': '/secure', + }) + + next_dispatcher + The next dispatcher object in the dispatch chain. + The VirtualHost dispatcher adds a prefix to the URL and calls + another dispatcher. Defaults to cherrypy.dispatch.Dispatcher(). + + use_x_forwarded_host + If True (the default), any "X-Forwarded-Host" + request header will be used instead of the "Host" header. This + is commonly added by HTTP servers (such as Apache) when proxying. + + ``**domains`` + A dict of {host header value: virtual prefix} pairs. + The incoming "Host" request header is looked up in this dict, + and, if a match is found, the corresponding "virtual prefix" + value will be prepended to the URL path before calling the + next dispatcher. Note that you often need separate entries + for "example.com" and "www.example.com". In addition, "Host" + headers may contain the port number. + """ + from cherrypy.lib import httputil + + def vhost_dispatch(path_info): + request = cherrypy.serving.request + header = request.headers.get + + domain = header('Host', '') + if use_x_forwarded_host: + domain = header('X-Forwarded-Host', domain) + + prefix = domains.get(domain, '') + if prefix: + path_info = httputil.urljoin(prefix, path_info) + + result = next_dispatcher(path_info) + + # Touch up staticdir config. See + # https://github.com/cherrypy/cherrypy/issues/614. + section = request.config.get('tools.staticdir.section') + if section: + section = section[len(prefix):] + request.config['tools.staticdir.section'] = section + + return result + return vhost_dispatch diff --git a/resources/lib/cherrypy/_cperror.py b/resources/lib/cherrypy/_cperror.py new file mode 100644 index 0000000..e2a8fad --- /dev/null +++ b/resources/lib/cherrypy/_cperror.py @@ -0,0 +1,619 @@ +"""Exception classes for CherryPy. + +CherryPy provides (and uses) exceptions for declaring that the HTTP response +should be a status other than the default "200 OK". You can ``raise`` them like +normal Python exceptions. You can also call them and they will raise +themselves; this means you can set an +:class:`HTTPError` +or :class:`HTTPRedirect` as the +:attr:`request.handler`. + +.. _redirectingpost: + +Redirecting POST +================ + +When you GET a resource and are redirected by the server to another Location, +there's generally no problem since GET is both a "safe method" (there should +be no side-effects) and an "idempotent method" (multiple calls are no different +than a single call). + +POST, however, is neither safe nor idempotent--if you +charge a credit card, you don't want to be charged twice by a redirect! + +For this reason, *none* of the 3xx responses permit a user-agent (browser) to +resubmit a POST on redirection without first confirming the action with the +user: + +===== ================================= =========== +300 Multiple Choices Confirm with the user +301 Moved Permanently Confirm with the user +302 Found (Object moved temporarily) Confirm with the user +303 See Other GET the new URI; no confirmation +304 Not modified for conditional GET only; + POST should not raise this error +305 Use Proxy Confirm with the user +307 Temporary Redirect Confirm with the user +===== ================================= =========== + +However, browsers have historically implemented these restrictions poorly; +in particular, many browsers do not force the user to confirm 301, 302 +or 307 when redirecting POST. For this reason, CherryPy defaults to 303, +which most user-agents appear to have implemented correctly. Therefore, if +you raise HTTPRedirect for a POST request, the user-agent will most likely +attempt to GET the new URI (without asking for confirmation from the user). +We realize this is confusing for developers, but it's the safest thing we +could do. You are of course free to raise ``HTTPRedirect(uri, status=302)`` +or any other 3xx status if you know what you're doing, but given the +environment, we couldn't let any of those be the default. + +Custom Error Handling +===================== + +.. image:: /refman/cperrors.gif + +Anticipated HTTP responses +-------------------------- + +The 'error_page' config namespace can be used to provide custom HTML output for +expected responses (like 404 Not Found). Supply a filename from which the +output will be read. The contents will be interpolated with the values +%(status)s, %(message)s, %(traceback)s, and %(version)s using plain old Python +`string formatting +`_. + +:: + + _cp_config = { + 'error_page.404': os.path.join(localDir, "static/index.html") + } + + +Beginning in version 3.1, you may also provide a function or other callable as +an error_page entry. It will be passed the same status, message, traceback and +version arguments that are interpolated into templates:: + + def error_page_402(status, message, traceback, version): + return "Error %s - Well, I'm very sorry but you haven't paid!" % status + cherrypy.config.update({'error_page.402': error_page_402}) + +Also in 3.1, in addition to the numbered error codes, you may also supply +"error_page.default" to handle all codes which do not have their own error_page +entry. + + + +Unanticipated errors +-------------------- + +CherryPy also has a generic error handling mechanism: whenever an unanticipated +error occurs in your code, it will call +:func:`Request.error_response` to +set the response status, headers, and body. By default, this is the same +output as +:class:`HTTPError(500) `. If you want to provide +some other behavior, you generally replace "request.error_response". + +Here is some sample code that shows how to display a custom error message and +send an e-mail containing the error:: + + from cherrypy import _cperror + + def handle_error(): + cherrypy.response.status = 500 + cherrypy.response.body = [ + "Sorry, an error occurred" + ] + sendMail('error@domain.com', + 'Error in your web app', + _cperror.format_exc()) + + @cherrypy.config(**{'request.error_response': handle_error}) + class Root: + pass + +Note that you have to explicitly set +:attr:`response.body ` +and not simply return an error message as a result. +""" + +import io +import contextlib +from sys import exc_info as _exc_info +from traceback import format_exception as _format_exception +from xml.sax import saxutils + +import six +from six.moves import urllib + +from more_itertools import always_iterable + +import cherrypy +from cherrypy._cpcompat import escape_html +from cherrypy._cpcompat import ntob +from cherrypy._cpcompat import tonative +from cherrypy._helper import classproperty +from cherrypy.lib import httputil as _httputil + + +class CherryPyException(Exception): + + """A base class for CherryPy exceptions.""" + pass + + +class InternalRedirect(CherryPyException): + + """Exception raised to switch to the handler for a different URL. + + This exception will redirect processing to another path within the site + (without informing the client). Provide the new path as an argument when + raising the exception. Provide any params in the querystring for the new + URL. + """ + + def __init__(self, path, query_string=''): + self.request = cherrypy.serving.request + + self.query_string = query_string + if '?' in path: + # Separate any params included in the path + path, self.query_string = path.split('?', 1) + + # Note that urljoin will "do the right thing" whether url is: + # 1. a URL relative to root (e.g. "/dummy") + # 2. a URL relative to the current path + # Note that any query string will be discarded. + path = urllib.parse.urljoin(self.request.path_info, path) + + # Set a 'path' member attribute so that code which traps this + # error can have access to it. + self.path = path + + CherryPyException.__init__(self, path, self.query_string) + + +class HTTPRedirect(CherryPyException): + + """Exception raised when the request should be redirected. + + This exception will force a HTTP redirect to the URL or URL's you give it. + The new URL must be passed as the first argument to the Exception, + e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list. + If a URL is absolute, it will be used as-is. If it is relative, it is + assumed to be relative to the current cherrypy.request.path_info. + + If one of the provided URL is a unicode object, it will be encoded + using the default encoding or the one passed in parameter. + + There are multiple types of redirect, from which you can select via the + ``status`` argument. If you do not provide a ``status`` arg, it defaults to + 303 (or 302 if responding with HTTP/1.0). + + Examples:: + + raise cherrypy.HTTPRedirect("") + raise cherrypy.HTTPRedirect("/abs/path", 307) + raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301) + + See :ref:`redirectingpost` for additional caveats. + """ + + urls = None + """The list of URL's to emit.""" + + encoding = 'utf-8' + """The encoding when passed urls are not native strings""" + + def __init__(self, urls, status=None, encoding=None): + self.urls = abs_urls = [ + # Note that urljoin will "do the right thing" whether url is: + # 1. a complete URL with host (e.g. "http://www.example.com/test") + # 2. a URL relative to root (e.g. "/dummy") + # 3. a URL relative to the current path + # Note that any query string in cherrypy.request is discarded. + urllib.parse.urljoin( + cherrypy.url(), + tonative(url, encoding or self.encoding), + ) + for url in always_iterable(urls) + ] + + status = ( + int(status) + if status is not None + else self.default_status + ) + if not 300 <= status <= 399: + raise ValueError('status must be between 300 and 399.') + + CherryPyException.__init__(self, abs_urls, status) + + @classproperty + def default_status(cls): + """ + The default redirect status for the request. + + RFC 2616 indicates a 301 response code fits our goal; however, + browser support for 301 is quite messy. Use 302/303 instead. See + http://www.alanflavell.org.uk/www/post-redirect.html + """ + return 303 if cherrypy.serving.request.protocol >= (1, 1) else 302 + + @property + def status(self): + """The integer HTTP status code to emit.""" + _, status = self.args[:2] + return status + + def set_response(self): + """Modify cherrypy.response status, headers, and body to represent + self. + + CherryPy uses this internally, but you can also use it to create an + HTTPRedirect object and set its output without *raising* the exception. + """ + response = cherrypy.serving.response + response.status = status = self.status + + if status in (300, 301, 302, 303, 307): + response.headers['Content-Type'] = 'text/html;charset=utf-8' + # "The ... URI SHOULD be given by the Location field + # in the response." + response.headers['Location'] = self.urls[0] + + # "Unless the request method was HEAD, the entity of the response + # SHOULD contain a short hypertext note with a hyperlink to the + # new URI(s)." + msg = { + 300: 'This resource can be found at ', + 301: 'This resource has permanently moved to ', + 302: 'This resource resides temporarily at ', + 303: 'This resource can be found at ', + 307: 'This resource has moved temporarily to ', + }[status] + msg += '%s.' + msgs = [ + msg % (saxutils.quoteattr(u), escape_html(u)) + for u in self.urls + ] + response.body = ntob('
\n'.join(msgs), 'utf-8') + # Previous code may have set C-L, so we have to reset it + # (allow finalize to set it). + response.headers.pop('Content-Length', None) + elif status == 304: + # Not Modified. + # "The response MUST include the following header fields: + # Date, unless its omission is required by section 14.18.1" + # The "Date" header should have been set in Response.__init__ + + # "...the response SHOULD NOT include other entity-headers." + for key in ('Allow', 'Content-Encoding', 'Content-Language', + 'Content-Length', 'Content-Location', 'Content-MD5', + 'Content-Range', 'Content-Type', 'Expires', + 'Last-Modified'): + if key in response.headers: + del response.headers[key] + + # "The 304 response MUST NOT contain a message-body." + response.body = None + # Previous code may have set C-L, so we have to reset it. + response.headers.pop('Content-Length', None) + elif status == 305: + # Use Proxy. + # self.urls[0] should be the URI of the proxy. + response.headers['Location'] = ntob(self.urls[0], 'utf-8') + response.body = None + # Previous code may have set C-L, so we have to reset it. + response.headers.pop('Content-Length', None) + else: + raise ValueError('The %s status code is unknown.' % status) + + def __call__(self): + """Use this exception as a request.handler (raise self).""" + raise self + + +def clean_headers(status): + """Remove any headers which should not apply to an error response.""" + response = cherrypy.serving.response + + # Remove headers which applied to the original content, + # but do not apply to the error page. + respheaders = response.headers + for key in ['Accept-Ranges', 'Age', 'ETag', 'Location', 'Retry-After', + 'Vary', 'Content-Encoding', 'Content-Length', 'Expires', + 'Content-Location', 'Content-MD5', 'Last-Modified']: + if key in respheaders: + del respheaders[key] + + if status != 416: + # A server sending a response with status code 416 (Requested + # range not satisfiable) SHOULD include a Content-Range field + # with a byte-range-resp-spec of "*". The instance-length + # specifies the current length of the selected resource. + # A response with status code 206 (Partial Content) MUST NOT + # include a Content-Range field with a byte-range- resp-spec of "*". + if 'Content-Range' in respheaders: + del respheaders['Content-Range'] + + +class HTTPError(CherryPyException): + + """Exception used to return an HTTP error code (4xx-5xx) to the client. + + This exception can be used to automatically send a response using a + http status code, with an appropriate error page. It takes an optional + ``status`` argument (which must be between 400 and 599); it defaults to 500 + ("Internal Server Error"). It also takes an optional ``message`` argument, + which will be returned in the response body. See + `RFC2616 `_ + for a complete list of available error codes and when to use them. + + Examples:: + + raise cherrypy.HTTPError(403) + raise cherrypy.HTTPError( + "403 Forbidden", "You are not allowed to access this resource.") + """ + + status = None + """The HTTP status code. May be of type int or str (with a Reason-Phrase). + """ + + code = None + """The integer HTTP status code.""" + + reason = None + """The HTTP Reason-Phrase string.""" + + def __init__(self, status=500, message=None): + self.status = status + try: + self.code, self.reason, defaultmsg = _httputil.valid_status(status) + except ValueError: + raise self.__class__(500, _exc_info()[1].args[0]) + + if self.code < 400 or self.code > 599: + raise ValueError('status must be between 400 and 599.') + + # See http://www.python.org/dev/peps/pep-0352/ + # self.message = message + self._message = message or defaultmsg + CherryPyException.__init__(self, status, message) + + def set_response(self): + """Modify cherrypy.response status, headers, and body to represent + self. + + CherryPy uses this internally, but you can also use it to create an + HTTPError object and set its output without *raising* the exception. + """ + response = cherrypy.serving.response + + clean_headers(self.code) + + # In all cases, finalize will be called after this method, + # so don't bother cleaning up response values here. + response.status = self.status + tb = None + if cherrypy.serving.request.show_tracebacks: + tb = format_exc() + + response.headers.pop('Content-Length', None) + + content = self.get_error_page(self.status, traceback=tb, + message=self._message) + response.body = content + + _be_ie_unfriendly(self.code) + + def get_error_page(self, *args, **kwargs): + return get_error_page(*args, **kwargs) + + def __call__(self): + """Use this exception as a request.handler (raise self).""" + raise self + + @classmethod + @contextlib.contextmanager + def handle(cls, exception, status=500, message=''): + """Translate exception into an HTTPError.""" + try: + yield + except exception as exc: + raise cls(status, message or str(exc)) + + +class NotFound(HTTPError): + + """Exception raised when a URL could not be mapped to any handler (404). + + This is equivalent to raising + :class:`HTTPError("404 Not Found") `. + """ + + def __init__(self, path=None): + if path is None: + request = cherrypy.serving.request + path = request.script_name + request.path_info + self.args = (path,) + HTTPError.__init__(self, 404, "The path '%s' was not found." % path) + + +_HTTPErrorTemplate = ''' + + + + %(status)s + + + +

%(status)s

+

%(message)s

+
%(traceback)s
+
+ + Powered by CherryPy %(version)s + +
+ + +''' + + +def get_error_page(status, **kwargs): + """Return an HTML page, containing a pretty error response. + + status should be an int or a str. + kwargs will be interpolated into the page template. + """ + try: + code, reason, message = _httputil.valid_status(status) + except ValueError: + raise cherrypy.HTTPError(500, _exc_info()[1].args[0]) + + # We can't use setdefault here, because some + # callers send None for kwarg values. + if kwargs.get('status') is None: + kwargs['status'] = '%s %s' % (code, reason) + if kwargs.get('message') is None: + kwargs['message'] = message + if kwargs.get('traceback') is None: + kwargs['traceback'] = '' + if kwargs.get('version') is None: + kwargs['version'] = cherrypy.__version__ + + for k, v in six.iteritems(kwargs): + if v is None: + kwargs[k] = '' + else: + kwargs[k] = escape_html(kwargs[k]) + + # Use a custom template or callable for the error page? + pages = cherrypy.serving.request.error_page + error_page = pages.get(code) or pages.get('default') + + # Default template, can be overridden below. + template = _HTTPErrorTemplate + if error_page: + try: + if hasattr(error_page, '__call__'): + # The caller function may be setting headers manually, + # so we delegate to it completely. We may be returning + # an iterator as well as a string here. + # + # We *must* make sure any content is not unicode. + result = error_page(**kwargs) + if cherrypy.lib.is_iterator(result): + from cherrypy.lib.encoding import UTF8StreamEncoder + return UTF8StreamEncoder(result) + elif isinstance(result, six.text_type): + return result.encode('utf-8') + else: + if not isinstance(result, bytes): + raise ValueError( + 'error page function did not ' + 'return a bytestring, six.text_type or an ' + 'iterator - returned object of type %s.' + % (type(result).__name__)) + return result + else: + # Load the template from this path. + template = io.open(error_page, newline='').read() + except Exception: + e = _format_exception(*_exc_info())[-1] + m = kwargs['message'] + if m: + m += '
' + m += 'In addition, the custom error page failed:\n
%s' % e + kwargs['message'] = m + + response = cherrypy.serving.response + response.headers['Content-Type'] = 'text/html;charset=utf-8' + result = template % kwargs + return result.encode('utf-8') + + +_ie_friendly_error_sizes = { + 400: 512, 403: 256, 404: 512, 405: 256, + 406: 512, 408: 512, 409: 512, 410: 256, + 500: 512, 501: 512, 505: 512, +} + + +def _be_ie_unfriendly(status): + response = cherrypy.serving.response + + # For some statuses, Internet Explorer 5+ shows "friendly error + # messages" instead of our response.body if the body is smaller + # than a given size. Fix this by returning a body over that size + # (by adding whitespace). + # See http://support.microsoft.com/kb/q218155/ + s = _ie_friendly_error_sizes.get(status, 0) + if s: + s += 1 + # Since we are issuing an HTTP error status, we assume that + # the entity is short, and we should just collapse it. + content = response.collapse_body() + content_length = len(content) + if content_length and content_length < s: + # IN ADDITION: the response must be written to IE + # in one chunk or it will still get replaced! Bah. + content = content + (b' ' * (s - content_length)) + response.body = content + response.headers['Content-Length'] = str(len(content)) + + +def format_exc(exc=None): + """Return exc (or sys.exc_info if None), formatted.""" + try: + if exc is None: + exc = _exc_info() + if exc == (None, None, None): + return '' + import traceback + return ''.join(traceback.format_exception(*exc)) + finally: + del exc + + +def bare_error(extrabody=None): + """Produce status, headers, body for a critical error. + + Returns a triple without calling any other questionable functions, + so it should be as error-free as possible. Call it from an HTTP server + if you get errors outside of the request. + + If extrabody is None, a friendly but rather unhelpful error message + is set in the body. If extrabody is a string, it will be appended + as-is to the body. + """ + + # The whole point of this function is to be a last line-of-defense + # in handling errors. That is, it must not raise any errors itself; + # it cannot be allowed to fail. Therefore, don't add to it! + # In particular, don't call any other CP functions. + + body = b'Unrecoverable error in the server.' + if extrabody is not None: + if not isinstance(extrabody, bytes): + extrabody = extrabody.encode('utf-8') + body += b'\n' + extrabody + + return (b'500 Internal Server Error', + [(b'Content-Type', b'text/plain'), + (b'Content-Length', ntob(str(len(body)), 'ISO-8859-1'))], + [body]) diff --git a/resources/lib/cherrypy/_cplogging.py b/resources/lib/cherrypy/_cplogging.py new file mode 100644 index 0000000..53b9add --- /dev/null +++ b/resources/lib/cherrypy/_cplogging.py @@ -0,0 +1,482 @@ +""" +Simple config +============= + +Although CherryPy uses the :mod:`Python logging module `, it does so +behind the scenes so that simple logging is simple, but complicated logging +is still possible. "Simple" logging means that you can log to the screen +(i.e. console/stdout) or to a file, and that you can easily have separate +error and access log files. + +Here are the simplified logging settings. You use these by adding lines to +your config file or dict. You should set these at either the global level or +per application (see next), but generally not both. + + * ``log.screen``: Set this to True to have both "error" and "access" messages + printed to stdout. + * ``log.access_file``: Set this to an absolute filename where you want + "access" messages written. + * ``log.error_file``: Set this to an absolute filename where you want "error" + messages written. + +Many events are automatically logged; to log your own application events, call +:func:`cherrypy.log`. + +Architecture +============ + +Separate scopes +--------------- + +CherryPy provides log managers at both the global and application layers. +This means you can have one set of logging rules for your entire site, +and another set of rules specific to each application. The global log +manager is found at :func:`cherrypy.log`, and the log manager for each +application is found at :attr:`app.log`. +If you're inside a request, the latter is reachable from +``cherrypy.request.app.log``; if you're outside a request, you'll have to +obtain a reference to the ``app``: either the return value of +:func:`tree.mount()` or, if you used +:func:`quickstart()` instead, via +``cherrypy.tree.apps['/']``. + +By default, the global logs are named "cherrypy.error" and "cherrypy.access", +and the application logs are named "cherrypy.error.2378745" and +"cherrypy.access.2378745" (the number is the id of the Application object). +This means that the application logs "bubble up" to the site logs, so if your +application has no log handlers, the site-level handlers will still log the +messages. + +Errors vs. Access +----------------- + +Each log manager handles both "access" messages (one per HTTP request) and +"error" messages (everything else). Note that the "error" log is not just for +errors! The format of access messages is highly formalized, but the error log +isn't--it receives messages from a variety of sources (including full error +tracebacks, if enabled). + +If you are logging the access log and error log to the same source, then there +is a possibility that a specially crafted error message may replicate an access +log message as described in CWE-117. In this case it is the application +developer's responsibility to manually escape data before +using CherryPy's log() +functionality, or they may create an application that is vulnerable to CWE-117. +This would be achieved by using a custom handler escape any special characters, +and attached as described below. + +Custom Handlers +=============== + +The simple settings above work by manipulating Python's standard :mod:`logging` +module. So when you need something more complex, the full power of the standard +module is yours to exploit. You can borrow or create custom handlers, formats, +filters, and much more. Here's an example that skips the standard FileHandler +and uses a RotatingFileHandler instead: + +:: + + #python + log = app.log + + # Remove the default FileHandlers if present. + log.error_file = "" + log.access_file = "" + + maxBytes = getattr(log, "rot_maxBytes", 10000000) + backupCount = getattr(log, "rot_backupCount", 1000) + + # Make a new RotatingFileHandler for the error log. + fname = getattr(log, "rot_error_file", "error.log") + h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount) + h.setLevel(DEBUG) + h.setFormatter(_cplogging.logfmt) + log.error_log.addHandler(h) + + # Make a new RotatingFileHandler for the access log. + fname = getattr(log, "rot_access_file", "access.log") + h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount) + h.setLevel(DEBUG) + h.setFormatter(_cplogging.logfmt) + log.access_log.addHandler(h) + + +The ``rot_*`` attributes are pulled straight from the application log object. +Since "log.*" config entries simply set attributes on the log object, you can +add custom attributes to your heart's content. Note that these handlers are +used ''instead'' of the default, simple handlers outlined above (so don't set +the "log.error_file" config entry, for example). +""" + +import datetime +import logging +import os +import sys + +import six + +import cherrypy +from cherrypy import _cperror + + +# Silence the no-handlers "warning" (stderr write!) in stdlib logging +logging.Logger.manager.emittedNoHandlerWarning = 1 +logfmt = logging.Formatter('%(message)s') + + +class NullHandler(logging.Handler): + + """A no-op logging handler to silence the logging.lastResort handler.""" + + def handle(self, record): + pass + + def emit(self, record): + pass + + def createLock(self): + self.lock = None + + +class LogManager(object): + + """An object to assist both simple and advanced logging. + + ``cherrypy.log`` is an instance of this class. + """ + + appid = None + """The id() of the Application object which owns this log manager. If this + is a global log manager, appid is None.""" + + error_log = None + """The actual :class:`logging.Logger` instance for error messages.""" + + access_log = None + """The actual :class:`logging.Logger` instance for access messages.""" + + access_log_format = ( + '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"' + if six.PY3 else + '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' + ) + + logger_root = None + """The "top-level" logger name. + + This string will be used as the first segment in the Logger names. + The default is "cherrypy", for example, in which case the Logger names + will be of the form:: + + cherrypy.error. + cherrypy.access. + """ + + def __init__(self, appid=None, logger_root='cherrypy'): + self.logger_root = logger_root + self.appid = appid + if appid is None: + self.error_log = logging.getLogger('%s.error' % logger_root) + self.access_log = logging.getLogger('%s.access' % logger_root) + else: + self.error_log = logging.getLogger( + '%s.error.%s' % (logger_root, appid)) + self.access_log = logging.getLogger( + '%s.access.%s' % (logger_root, appid)) + self.error_log.setLevel(logging.INFO) + self.access_log.setLevel(logging.INFO) + + # Silence the no-handlers "warning" (stderr write!) in stdlib logging + self.error_log.addHandler(NullHandler()) + self.access_log.addHandler(NullHandler()) + + cherrypy.engine.subscribe('graceful', self.reopen_files) + + def reopen_files(self): + """Close and reopen all file handlers.""" + for log in (self.error_log, self.access_log): + for h in log.handlers: + if isinstance(h, logging.FileHandler): + h.acquire() + h.stream.close() + h.stream = open(h.baseFilename, h.mode) + h.release() + + def error(self, msg='', context='', severity=logging.INFO, + traceback=False): + """Write the given ``msg`` to the error log. + + This is not just for errors! Applications may call this at any time + to log application-specific information. + + If ``traceback`` is True, the traceback of the current exception + (if any) will be appended to ``msg``. + """ + exc_info = None + if traceback: + exc_info = _cperror._exc_info() + + self.error_log.log( + severity, + ' '.join((self.time(), context, msg)), + exc_info=exc_info, + ) + + def __call__(self, *args, **kwargs): + """An alias for ``error``.""" + return self.error(*args, **kwargs) + + def access(self): + """Write to the access log (in Apache/NCSA Combined Log format). + + See the + `apache documentation + `_ + for format details. + + CherryPy calls this automatically for you. Note there are no arguments; + it collects the data itself from + :class:`cherrypy.request`. + + Like Apache started doing in 2.0.46, non-printable and other special + characters in %r (and we expand that to all parts) are escaped using + \\xhh sequences, where hh stands for the hexadecimal representation + of the raw byte. Exceptions from this rule are " and \\, which are + escaped by prepending a backslash, and all whitespace characters, + which are written in their C-style notation (\\n, \\t, etc). + """ + request = cherrypy.serving.request + remote = request.remote + response = cherrypy.serving.response + outheaders = response.headers + inheaders = request.headers + if response.output_status is None: + status = '-' + else: + status = response.output_status.split(b' ', 1)[0] + if six.PY3: + status = status.decode('ISO-8859-1') + + atoms = {'h': remote.name or remote.ip, + 'l': '-', + 'u': getattr(request, 'login', None) or '-', + 't': self.time(), + 'r': request.request_line, + 's': status, + 'b': dict.get(outheaders, 'Content-Length', '') or '-', + 'f': dict.get(inheaders, 'Referer', ''), + 'a': dict.get(inheaders, 'User-Agent', ''), + 'o': dict.get(inheaders, 'Host', '-'), + 'i': request.unique_id, + 'z': LazyRfc3339UtcTime(), + } + if six.PY3: + for k, v in atoms.items(): + if not isinstance(v, str): + v = str(v) + v = v.replace('"', '\\"').encode('utf8') + # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc + # and backslash for us. All we have to do is strip the quotes. + v = repr(v)[2:-1] + + # in python 3.0 the repr of bytes (as returned by encode) + # uses double \'s. But then the logger escapes them yet, again + # resulting in quadruple slashes. Remove the extra one here. + v = v.replace('\\\\', '\\') + + # Escape double-quote. + atoms[k] = v + + try: + self.access_log.log( + logging.INFO, self.access_log_format.format(**atoms)) + except Exception: + self(traceback=True) + else: + for k, v in atoms.items(): + if isinstance(v, six.text_type): + v = v.encode('utf8') + elif not isinstance(v, str): + v = str(v) + # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc + # and backslash for us. All we have to do is strip the quotes. + v = repr(v)[1:-1] + # Escape double-quote. + atoms[k] = v.replace('"', '\\"') + + try: + self.access_log.log( + logging.INFO, self.access_log_format % atoms) + except Exception: + self(traceback=True) + + def time(self): + """Return now() in Apache Common Log Format (no timezone).""" + now = datetime.datetime.now() + monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', + 'jul', 'aug', 'sep', 'oct', 'nov', 'dec'] + month = monthnames[now.month - 1].capitalize() + return ('[%02d/%s/%04d:%02d:%02d:%02d]' % + (now.day, month, now.year, now.hour, now.minute, now.second)) + + def _get_builtin_handler(self, log, key): + for h in log.handlers: + if getattr(h, '_cpbuiltin', None) == key: + return h + + # ------------------------- Screen handlers ------------------------- # + def _set_screen_handler(self, log, enable, stream=None): + h = self._get_builtin_handler(log, 'screen') + if enable: + if not h: + if stream is None: + stream = sys.stderr + h = logging.StreamHandler(stream) + h.setFormatter(logfmt) + h._cpbuiltin = 'screen' + log.addHandler(h) + elif h: + log.handlers.remove(h) + + @property + def screen(self): + """Turn stderr/stdout logging on or off. + + If you set this to True, it'll add the appropriate StreamHandler for + you. If you set it to False, it will remove the handler. + """ + h = self._get_builtin_handler + has_h = h(self.error_log, 'screen') or h(self.access_log, 'screen') + return bool(has_h) + + @screen.setter + def screen(self, newvalue): + self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr) + self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout) + + # -------------------------- File handlers -------------------------- # + + def _add_builtin_file_handler(self, log, fname): + h = logging.FileHandler(fname) + h.setFormatter(logfmt) + h._cpbuiltin = 'file' + log.addHandler(h) + + def _set_file_handler(self, log, filename): + h = self._get_builtin_handler(log, 'file') + if filename: + if h: + if h.baseFilename != os.path.abspath(filename): + h.close() + log.handlers.remove(h) + self._add_builtin_file_handler(log, filename) + else: + self._add_builtin_file_handler(log, filename) + else: + if h: + h.close() + log.handlers.remove(h) + + @property + def error_file(self): + """The filename for self.error_log. + + If you set this to a string, it'll add the appropriate FileHandler for + you. If you set it to ``None`` or ``''``, it will remove the handler. + """ + h = self._get_builtin_handler(self.error_log, 'file') + if h: + return h.baseFilename + return '' + + @error_file.setter + def error_file(self, newvalue): + self._set_file_handler(self.error_log, newvalue) + + @property + def access_file(self): + """The filename for self.access_log. + + If you set this to a string, it'll add the appropriate FileHandler for + you. If you set it to ``None`` or ``''``, it will remove the handler. + """ + h = self._get_builtin_handler(self.access_log, 'file') + if h: + return h.baseFilename + return '' + + @access_file.setter + def access_file(self, newvalue): + self._set_file_handler(self.access_log, newvalue) + + # ------------------------- WSGI handlers ------------------------- # + + def _set_wsgi_handler(self, log, enable): + h = self._get_builtin_handler(log, 'wsgi') + if enable: + if not h: + h = WSGIErrorHandler() + h.setFormatter(logfmt) + h._cpbuiltin = 'wsgi' + log.addHandler(h) + elif h: + log.handlers.remove(h) + + @property + def wsgi(self): + """Write errors to wsgi.errors. + + If you set this to True, it'll add the appropriate + :class:`WSGIErrorHandler` for you + (which writes errors to ``wsgi.errors``). + If you set it to False, it will remove the handler. + """ + return bool(self._get_builtin_handler(self.error_log, 'wsgi')) + + @wsgi.setter + def wsgi(self, newvalue): + self._set_wsgi_handler(self.error_log, newvalue) + + +class WSGIErrorHandler(logging.Handler): + + "A handler class which writes logging records to environ['wsgi.errors']." + + def flush(self): + """Flushes the stream.""" + try: + stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors') + except (AttributeError, KeyError): + pass + else: + stream.flush() + + def emit(self, record): + """Emit a record.""" + try: + stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors') + except (AttributeError, KeyError): + pass + else: + try: + msg = self.format(record) + fs = '%s\n' + import types + # if no unicode support... + if not hasattr(types, 'UnicodeType'): + stream.write(fs % msg) + else: + try: + stream.write(fs % msg) + except UnicodeError: + stream.write(fs % msg.encode('UTF-8')) + self.flush() + except Exception: + self.handleError(record) + + +class LazyRfc3339UtcTime(object): + def __str__(self): + """Return now() in RFC3339 UTC Format.""" + now = datetime.datetime.now() + return now.isoformat('T') + 'Z' diff --git a/resources/lib/cherrypy/_cpmodpy.py b/resources/lib/cherrypy/_cpmodpy.py new file mode 100644 index 0000000..ac91e62 --- /dev/null +++ b/resources/lib/cherrypy/_cpmodpy.py @@ -0,0 +1,356 @@ +"""Native adapter for serving CherryPy via mod_python + +Basic usage: + +########################################## +# Application in a module called myapp.py +########################################## + +import cherrypy + +class Root: + @cherrypy.expose + def index(self): + return 'Hi there, Ho there, Hey there' + + +# We will use this method from the mod_python configuration +# as the entry point to our application +def setup_server(): + cherrypy.tree.mount(Root()) + cherrypy.config.update({'environment': 'production', + 'log.screen': False, + 'show_tracebacks': False}) + +########################################## +# mod_python settings for apache2 +# This should reside in your httpd.conf +# or a file that will be loaded at +# apache startup +########################################## + +# Start +DocumentRoot "/" +Listen 8080 +LoadModule python_module /usr/lib/apache2/modules/mod_python.so + + + PythonPath "sys.path+['/path/to/my/application']" + SetHandler python-program + PythonHandler cherrypy._cpmodpy::handler + PythonOption cherrypy.setup myapp::setup_server + PythonDebug On + +# End + +The actual path to your mod_python.so is dependent on your +environment. In this case we suppose a global mod_python +installation on a Linux distribution such as Ubuntu. + +We do set the PythonPath configuration setting so that +your application can be found by from the user running +the apache2 instance. Of course if your application +resides in the global site-package this won't be needed. + +Then restart apache2 and access http://127.0.0.1:8080 +""" + +import io +import logging +import os +import re +import sys + +import six + +from more_itertools import always_iterable + +import cherrypy +from cherrypy._cperror import format_exc, bare_error +from cherrypy.lib import httputil + + +# ------------------------------ Request-handling + + +def setup(req): + from mod_python import apache + + # Run any setup functions defined by a "PythonOption cherrypy.setup" + # directive. + options = req.get_options() + if 'cherrypy.setup' in options: + for function in options['cherrypy.setup'].split(): + atoms = function.split('::', 1) + if len(atoms) == 1: + mod = __import__(atoms[0], globals(), locals()) + else: + modname, fname = atoms + mod = __import__(modname, globals(), locals(), [fname]) + func = getattr(mod, fname) + func() + + cherrypy.config.update({'log.screen': False, + 'tools.ignore_headers.on': True, + 'tools.ignore_headers.headers': ['Range'], + }) + + engine = cherrypy.engine + if hasattr(engine, 'signal_handler'): + engine.signal_handler.unsubscribe() + if hasattr(engine, 'console_control_handler'): + engine.console_control_handler.unsubscribe() + engine.autoreload.unsubscribe() + cherrypy.server.unsubscribe() + + @engine.subscribe('log') + def _log(msg, level): + newlevel = apache.APLOG_ERR + if logging.DEBUG >= level: + newlevel = apache.APLOG_DEBUG + elif logging.INFO >= level: + newlevel = apache.APLOG_INFO + elif logging.WARNING >= level: + newlevel = apache.APLOG_WARNING + # On Windows, req.server is required or the msg will vanish. See + # http://www.modpython.org/pipermail/mod_python/2003-October/014291.html + # Also, "When server is not specified...LogLevel does not apply..." + apache.log_error(msg, newlevel, req.server) + + engine.start() + + def cherrypy_cleanup(data): + engine.exit() + try: + # apache.register_cleanup wasn't available until 3.1.4. + apache.register_cleanup(cherrypy_cleanup) + except AttributeError: + req.server.register_cleanup(req, cherrypy_cleanup) + + +class _ReadOnlyRequest: + expose = ('read', 'readline', 'readlines') + + def __init__(self, req): + for method in self.expose: + self.__dict__[method] = getattr(req, method) + + +recursive = False + +_isSetUp = False + + +def handler(req): + from mod_python import apache + try: + global _isSetUp + if not _isSetUp: + setup(req) + _isSetUp = True + + # Obtain a Request object from CherryPy + local = req.connection.local_addr + local = httputil.Host( + local[0], local[1], req.connection.local_host or '') + remote = req.connection.remote_addr + remote = httputil.Host( + remote[0], remote[1], req.connection.remote_host or '') + + scheme = req.parsed_uri[0] or 'http' + req.get_basic_auth_pw() + + try: + # apache.mpm_query only became available in mod_python 3.1 + q = apache.mpm_query + threaded = q(apache.AP_MPMQ_IS_THREADED) + forked = q(apache.AP_MPMQ_IS_FORKED) + except AttributeError: + bad_value = ("You must provide a PythonOption '%s', " + "either 'on' or 'off', when running a version " + 'of mod_python < 3.1') + + options = req.get_options() + + threaded = options.get('multithread', '').lower() + if threaded == 'on': + threaded = True + elif threaded == 'off': + threaded = False + else: + raise ValueError(bad_value % 'multithread') + + forked = options.get('multiprocess', '').lower() + if forked == 'on': + forked = True + elif forked == 'off': + forked = False + else: + raise ValueError(bad_value % 'multiprocess') + + sn = cherrypy.tree.script_name(req.uri or '/') + if sn is None: + send_response(req, '404 Not Found', [], '') + else: + app = cherrypy.tree.apps[sn] + method = req.method + path = req.uri + qs = req.args or '' + reqproto = req.protocol + headers = list(six.iteritems(req.headers_in)) + rfile = _ReadOnlyRequest(req) + prev = None + + try: + redirections = [] + while True: + request, response = app.get_serving(local, remote, scheme, + 'HTTP/1.1') + request.login = req.user + request.multithread = bool(threaded) + request.multiprocess = bool(forked) + request.app = app + request.prev = prev + + # Run the CherryPy Request object and obtain the response + try: + request.run(method, path, qs, reqproto, headers, rfile) + break + except cherrypy.InternalRedirect: + ir = sys.exc_info()[1] + app.release_serving() + prev = request + + if not recursive: + if ir.path in redirections: + raise RuntimeError( + 'InternalRedirector visited the same URL ' + 'twice: %r' % ir.path) + else: + # Add the *previous* path_info + qs to + # redirections. + if qs: + qs = '?' + qs + redirections.append(sn + path + qs) + + # Munge environment and try again. + method = 'GET' + path = ir.path + qs = ir.query_string + rfile = io.BytesIO() + + send_response( + req, response.output_status, response.header_list, + response.body, response.stream) + finally: + app.release_serving() + except Exception: + tb = format_exc() + cherrypy.log(tb, 'MOD_PYTHON', severity=logging.ERROR) + s, h, b = bare_error() + send_response(req, s, h, b) + return apache.OK + + +def send_response(req, status, headers, body, stream=False): + # Set response status + req.status = int(status[:3]) + + # Set response headers + req.content_type = 'text/plain' + for header, value in headers: + if header.lower() == 'content-type': + req.content_type = value + continue + req.headers_out.add(header, value) + + if stream: + # Flush now so the status and headers are sent immediately. + req.flush() + + # Set response body + for seg in always_iterable(body): + req.write(seg) + + +# --------------- Startup tools for CherryPy + mod_python --------------- # +try: + import subprocess + + def popen(fullcmd): + p = subprocess.Popen(fullcmd, shell=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT, + close_fds=True) + return p.stdout +except ImportError: + def popen(fullcmd): + pipein, pipeout = os.popen4(fullcmd) + return pipeout + + +def read_process(cmd, args=''): + fullcmd = '%s %s' % (cmd, args) + pipeout = popen(fullcmd) + try: + firstline = pipeout.readline() + cmd_not_found = re.search( + b'(not recognized|No such file|not found)', + firstline, + re.IGNORECASE + ) + if cmd_not_found: + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +class ModPythonServer(object): + + template = """ +# Apache2 server configuration file for running CherryPy with mod_python. + +DocumentRoot "/" +Listen %(port)s +LoadModule python_module modules/mod_python.so + + + SetHandler python-program + PythonHandler %(handler)s + PythonDebug On +%(opts)s + +""" + + def __init__(self, loc='/', port=80, opts=None, apache_path='apache', + handler='cherrypy._cpmodpy::handler'): + self.loc = loc + self.port = port + self.opts = opts + self.apache_path = apache_path + self.handler = handler + + def start(self): + opts = ''.join([' PythonOption %s %s\n' % (k, v) + for k, v in self.opts]) + conf_data = self.template % {'port': self.port, + 'loc': self.loc, + 'opts': opts, + 'handler': self.handler, + } + + mpconf = os.path.join(os.path.dirname(__file__), 'cpmodpy.conf') + f = open(mpconf, 'wb') + try: + f.write(conf_data) + finally: + f.close() + + response = read_process(self.apache_path, '-k start -f %s' % mpconf) + self.ready = True + return response + + def stop(self): + os.popen('apache -k stop') + self.ready = False diff --git a/resources/lib/cherrypy/_cpnative_server.py b/resources/lib/cherrypy/_cpnative_server.py new file mode 100644 index 0000000..e9671d2 --- /dev/null +++ b/resources/lib/cherrypy/_cpnative_server.py @@ -0,0 +1,168 @@ +"""Native adapter for serving CherryPy via its builtin server.""" + +import logging +import sys +import io + +import cheroot.server + +import cherrypy +from cherrypy._cperror import format_exc, bare_error +from cherrypy.lib import httputil +from ._cpcompat import tonative + + +class NativeGateway(cheroot.server.Gateway): + """Native gateway implementation allowing to bypass WSGI.""" + + recursive = False + + def respond(self): + """Obtain response from CherryPy machinery and then send it.""" + req = self.req + try: + # Obtain a Request object from CherryPy + local = req.server.bind_addr # FIXME: handle UNIX sockets + local = tonative(local[0]), local[1] + local = httputil.Host(local[0], local[1], '') + remote = tonative(req.conn.remote_addr), req.conn.remote_port + remote = httputil.Host(remote[0], remote[1], '') + + scheme = tonative(req.scheme) + sn = cherrypy.tree.script_name(tonative(req.uri or '/')) + if sn is None: + self.send_response('404 Not Found', [], ['']) + else: + app = cherrypy.tree.apps[sn] + method = tonative(req.method) + path = tonative(req.path) + qs = tonative(req.qs or '') + headers = ( + (tonative(h), tonative(v)) + for h, v in req.inheaders.items() + ) + rfile = req.rfile + prev = None + + try: + redirections = [] + while True: + request, response = app.get_serving( + local, remote, scheme, 'HTTP/1.1') + request.multithread = True + request.multiprocess = False + request.app = app + request.prev = prev + + # Run the CherryPy Request object and obtain the + # response + try: + request.run( + method, path, qs, + tonative(req.request_protocol), + headers, rfile, + ) + break + except cherrypy.InternalRedirect: + ir = sys.exc_info()[1] + app.release_serving() + prev = request + + if not self.recursive: + if ir.path in redirections: + raise RuntimeError( + 'InternalRedirector visited the same ' + 'URL twice: %r' % ir.path) + else: + # Add the *previous* path_info + qs to + # redirections. + if qs: + qs = '?' + qs + redirections.append(sn + path + qs) + + # Munge environment and try again. + method = 'GET' + path = ir.path + qs = ir.query_string + rfile = io.BytesIO() + + self.send_response( + response.output_status, response.header_list, + response.body) + finally: + app.release_serving() + except Exception: + tb = format_exc() + # print tb + cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR) + s, h, b = bare_error() + self.send_response(s, h, b) + + def send_response(self, status, headers, body): + """Send response to HTTP request.""" + req = self.req + + # Set response status + req.status = status or b'500 Server Error' + + # Set response headers + for header, value in headers: + req.outheaders.append((header, value)) + if (req.ready and not req.sent_headers): + req.sent_headers = True + req.send_headers() + + # Set response body + for seg in body: + req.write(seg) + + +class CPHTTPServer(cheroot.server.HTTPServer): + """Wrapper for cheroot.server.HTTPServer. + + cheroot has been designed to not reference CherryPy in any way, + so that it can be used in other frameworks and applications. + Therefore, we wrap it here, so we can apply some attributes + from config -> cherrypy.server -> HTTPServer. + """ + + def __init__(self, server_adapter=cherrypy.server): + """Initialize CPHTTPServer.""" + self.server_adapter = server_adapter + + server_name = (self.server_adapter.socket_host or + self.server_adapter.socket_file or + None) + + cheroot.server.HTTPServer.__init__( + self, server_adapter.bind_addr, NativeGateway, + minthreads=server_adapter.thread_pool, + maxthreads=server_adapter.thread_pool_max, + server_name=server_name) + + self.max_request_header_size = ( + self.server_adapter.max_request_header_size or 0) + self.max_request_body_size = ( + self.server_adapter.max_request_body_size or 0) + self.request_queue_size = self.server_adapter.socket_queue_size + self.timeout = self.server_adapter.socket_timeout + self.shutdown_timeout = self.server_adapter.shutdown_timeout + self.protocol = self.server_adapter.protocol_version + self.nodelay = self.server_adapter.nodelay + + ssl_module = self.server_adapter.ssl_module or 'pyopenssl' + if self.server_adapter.ssl_context: + adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain, + self.server_adapter.ssl_ciphers) + self.ssl_adapter.context = self.server_adapter.ssl_context + elif self.server_adapter.ssl_certificate: + adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain, + self.server_adapter.ssl_ciphers) diff --git a/resources/lib/cherrypy/_cpreqbody.py b/resources/lib/cherrypy/_cpreqbody.py new file mode 100644 index 0000000..893fe5f --- /dev/null +++ b/resources/lib/cherrypy/_cpreqbody.py @@ -0,0 +1,1000 @@ +"""Request body processing for CherryPy. + +.. versionadded:: 3.2 + +Application authors have complete control over the parsing of HTTP request +entities. In short, +:attr:`cherrypy.request.body` +is now always set to an instance of +:class:`RequestBody`, +and *that* class is a subclass of :class:`Entity`. + +When an HTTP request includes an entity body, it is often desirable to +provide that information to applications in a form other than the raw bytes. +Different content types demand different approaches. Examples: + + * For a GIF file, we want the raw bytes in a stream. + * An HTML form is better parsed into its component fields, and each text field + decoded from bytes to unicode. + * A JSON body should be deserialized into a Python dict or list. + +When the request contains a Content-Type header, the media type is used as a +key to look up a value in the +:attr:`request.body.processors` dict. +If the full media +type is not found, then the major type is tried; for example, if no processor +is found for the 'image/jpeg' type, then we look for a processor for the +'image' types altogether. If neither the full type nor the major type has a +matching processor, then a default processor is used +(:func:`default_proc`). For most +types, this means no processing is done, and the body is left unread as a +raw byte stream. Processors are configurable in an 'on_start_resource' hook. + +Some processors, especially those for the 'text' types, attempt to decode bytes +to unicode. If the Content-Type request header includes a 'charset' parameter, +this is used to decode the entity. Otherwise, one or more default charsets may +be attempted, although this decision is up to each processor. If a processor +successfully decodes an Entity or Part, it should set the +:attr:`charset` attribute +on the Entity or Part to the name of the successful charset, so that +applications can easily re-encode or transcode the value if they wish. + +If the Content-Type of the request entity is of major type 'multipart', then +the above parsing process, and possibly a decoding process, is performed for +each part. + +For both the full entity and multipart parts, a Content-Disposition header may +be used to fill :attr:`name` and +:attr:`filename` attributes on the +request.body or the Part. + +.. _custombodyprocessors: + +Custom Processors +================= + +You can add your own processors for any specific or major MIME type. Simply add +it to the :attr:`processors` dict in a +hook/tool that runs at ``on_start_resource`` or ``before_request_body``. +Here's the built-in JSON tool for an example:: + + def json_in(force=True, debug=False): + request = cherrypy.serving.request + def json_processor(entity): + '''Read application/json data into request.json.''' + if not entity.headers.get("Content-Length", ""): + raise cherrypy.HTTPError(411) + + body = entity.fp.read() + try: + request.json = json_decode(body) + except ValueError: + raise cherrypy.HTTPError(400, 'Invalid JSON document') + if force: + request.body.processors.clear() + request.body.default_proc = cherrypy.HTTPError( + 415, 'Expected an application/json content type') + request.body.processors['application/json'] = json_processor + +We begin by defining a new ``json_processor`` function to stick in the +``processors`` dictionary. All processor functions take a single argument, +the ``Entity`` instance they are to process. It will be called whenever a +request is received (for those URI's where the tool is turned on) which +has a ``Content-Type`` of "application/json". + +First, it checks for a valid ``Content-Length`` (raising 411 if not valid), +then reads the remaining bytes on the socket. The ``fp`` object knows its +own length, so it won't hang waiting for data that never arrives. It will +return when all data has been read. Then, we decode those bytes using +Python's built-in ``json`` module, and stick the decoded result onto +``request.json`` . If it cannot be decoded, we raise 400. + +If the "force" argument is True (the default), the ``Tool`` clears the +``processors`` dict so that request entities of other ``Content-Types`` +aren't parsed at all. Since there's no entry for those invalid MIME +types, the ``default_proc`` method of ``cherrypy.request.body`` is +called. But this does nothing by default (usually to provide the page +handler an opportunity to handle it.) +But in our case, we want to raise 415, so we replace +``request.body.default_proc`` +with the error (``HTTPError`` instances, when called, raise themselves). + +If we were defining a custom processor, we can do so without making a ``Tool``. +Just add the config entry:: + + request.body.processors = {'application/json': json_processor} + +Note that you can only replace the ``processors`` dict wholesale this way, +not update the existing one. +""" + +try: + from io import DEFAULT_BUFFER_SIZE +except ImportError: + DEFAULT_BUFFER_SIZE = 8192 +import re +import sys +import tempfile +try: + from urllib import unquote_plus +except ImportError: + def unquote_plus(bs): + """Bytes version of urllib.parse.unquote_plus.""" + bs = bs.replace(b'+', b' ') + atoms = bs.split(b'%') + for i in range(1, len(atoms)): + item = atoms[i] + try: + pct = int(item[:2], 16) + atoms[i] = bytes([pct]) + item[2:] + except ValueError: + pass + return b''.join(atoms) + +import six +import cheroot.server + +import cherrypy +from cherrypy._cpcompat import ntou, unquote +from cherrypy.lib import httputil + + +# ------------------------------- Processors -------------------------------- # + +def process_urlencoded(entity): + """Read application/x-www-form-urlencoded data into entity.params.""" + qs = entity.fp.read() + for charset in entity.attempt_charsets: + try: + params = {} + for aparam in qs.split(b'&'): + for pair in aparam.split(b';'): + if not pair: + continue + + atoms = pair.split(b'=', 1) + if len(atoms) == 1: + atoms.append(b'') + + key = unquote_plus(atoms[0]).decode(charset) + value = unquote_plus(atoms[1]).decode(charset) + + if key in params: + if not isinstance(params[key], list): + params[key] = [params[key]] + params[key].append(value) + else: + params[key] = value + except UnicodeDecodeError: + pass + else: + entity.charset = charset + break + else: + raise cherrypy.HTTPError( + 400, 'The request entity could not be decoded. The following ' + 'charsets were attempted: %s' % repr(entity.attempt_charsets)) + + # Now that all values have been successfully parsed and decoded, + # apply them to the entity.params dict. + for key, value in params.items(): + if key in entity.params: + if not isinstance(entity.params[key], list): + entity.params[key] = [entity.params[key]] + entity.params[key].append(value) + else: + entity.params[key] = value + + +def process_multipart(entity): + """Read all multipart parts into entity.parts.""" + ib = '' + if 'boundary' in entity.content_type.params: + # http://tools.ietf.org/html/rfc2046#section-5.1.1 + # "The grammar for parameters on the Content-type field is such that it + # is often necessary to enclose the boundary parameter values in quotes + # on the Content-type line" + ib = entity.content_type.params['boundary'].strip('"') + + if not re.match('^[ -~]{0,200}[!-~]$', ib): + raise ValueError('Invalid boundary in multipart form: %r' % (ib,)) + + ib = ('--' + ib).encode('ascii') + + # Find the first marker + while True: + b = entity.readline() + if not b: + return + + b = b.strip() + if b == ib: + break + + # Read all parts + while True: + part = entity.part_class.from_fp(entity.fp, ib) + entity.parts.append(part) + part.process() + if part.fp.done: + break + + +def process_multipart_form_data(entity): + """Read all multipart/form-data parts into entity.parts or entity.params. + """ + process_multipart(entity) + + kept_parts = [] + for part in entity.parts: + if part.name is None: + kept_parts.append(part) + else: + if part.filename is None: + # It's a regular field + value = part.fullvalue() + else: + # It's a file upload. Retain the whole part so consumer code + # has access to its .file and .filename attributes. + value = part + + if part.name in entity.params: + if not isinstance(entity.params[part.name], list): + entity.params[part.name] = [entity.params[part.name]] + entity.params[part.name].append(value) + else: + entity.params[part.name] = value + + entity.parts = kept_parts + + +def _old_process_multipart(entity): + """The behavior of 3.2 and lower. Deprecated and will be changed in 3.3.""" + process_multipart(entity) + + params = entity.params + + for part in entity.parts: + if part.name is None: + key = ntou('parts') + else: + key = part.name + + if part.filename is None: + # It's a regular field + value = part.fullvalue() + else: + # It's a file upload. Retain the whole part so consumer code + # has access to its .file and .filename attributes. + value = part + + if key in params: + if not isinstance(params[key], list): + params[key] = [params[key]] + params[key].append(value) + else: + params[key] = value + + +# -------------------------------- Entities --------------------------------- # +class Entity(object): + + """An HTTP request body, or MIME multipart body. + + This class collects information about the HTTP request entity. When a + given entity is of MIME type "multipart", each part is parsed into its own + Entity instance, and the set of parts stored in + :attr:`entity.parts`. + + Between the ``before_request_body`` and ``before_handler`` tools, CherryPy + tries to process the request body (if any) by calling + :func:`request.body.process`. + This uses the ``content_type`` of the Entity to look up a suitable + processor in + :attr:`Entity.processors`, + a dict. + If a matching processor cannot be found for the complete Content-Type, + it tries again using the major type. For example, if a request with an + entity of type "image/jpeg" arrives, but no processor can be found for + that complete type, then one is sought for the major type "image". If a + processor is still not found, then the + :func:`default_proc` method + of the Entity is called (which does nothing by default; you can + override this too). + + CherryPy includes processors for the "application/x-www-form-urlencoded" + type, the "multipart/form-data" type, and the "multipart" major type. + CherryPy 3.2 processes these types almost exactly as older versions. + Parts are passed as arguments to the page handler using their + ``Content-Disposition.name`` if given, otherwise in a generic "parts" + argument. Each such part is either a string, or the + :class:`Part` itself if it's a file. (In this + case it will have ``file`` and ``filename`` attributes, or possibly a + ``value`` attribute). Each Part is itself a subclass of + Entity, and has its own ``process`` method and ``processors`` dict. + + There is a separate processor for the "multipart" major type which is more + flexible, and simply stores all multipart parts in + :attr:`request.body.parts`. You can + enable it with:: + + cherrypy.request.body.processors['multipart'] = \ + _cpreqbody.process_multipart + + in an ``on_start_resource`` tool. + """ + + # http://tools.ietf.org/html/rfc2046#section-4.1.2: + # "The default character set, which must be assumed in the + # absence of a charset parameter, is US-ASCII." + # However, many browsers send data in utf-8 with no charset. + attempt_charsets = ['utf-8'] + r"""A list of strings, each of which should be a known encoding. + + When the Content-Type of the request body warrants it, each of the given + encodings will be tried in order. The first one to successfully decode the + entity without raising an error is stored as + :attr:`entity.charset`. This defaults + to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by + `HTTP/1.1 + `_), + but ``['us-ascii', 'utf-8']`` for multipart parts. + """ + + charset = None + """The successful decoding; see "attempt_charsets" above.""" + + content_type = None + """The value of the Content-Type request header. + + If the Entity is part of a multipart payload, this will be the Content-Type + given in the MIME headers for this part. + """ + + default_content_type = 'application/x-www-form-urlencoded' + """This defines a default ``Content-Type`` to use if no Content-Type header + is given. The empty string is used for RequestBody, which results in the + request body not being read or parsed at all. This is by design; a missing + ``Content-Type`` header in the HTTP request entity is an error at best, + and a security hole at worst. For multipart parts, however, the MIME spec + declares that a part with no Content-Type defaults to "text/plain" + (see :class:`Part`). + """ + + filename = None + """The ``Content-Disposition.filename`` header, if available.""" + + fp = None + """The readable socket file object.""" + + headers = None + """A dict of request/multipart header names and values. + + This is a copy of the ``request.headers`` for the ``request.body``; + for multipart parts, it is the set of headers for that part. + """ + + length = None + """The value of the ``Content-Length`` header, if provided.""" + + name = None + """The "name" parameter of the ``Content-Disposition`` header, if any.""" + + params = None + """ + If the request Content-Type is 'application/x-www-form-urlencoded' or + multipart, this will be a dict of the params pulled from the entity + body; that is, it will be the portion of request.params that come + from the message body (sometimes called "POST params", although they + can be sent with various HTTP method verbs). This value is set between + the 'before_request_body' and 'before_handler' hooks (assuming that + process_request_body is True).""" + + processors = {'application/x-www-form-urlencoded': process_urlencoded, + 'multipart/form-data': process_multipart_form_data, + 'multipart': process_multipart, + } + """A dict of Content-Type names to processor methods.""" + + parts = None + """A list of Part instances if ``Content-Type`` is of major type + "multipart".""" + + part_class = None + """The class used for multipart parts. + + You can replace this with custom subclasses to alter the processing of + multipart parts. + """ + + def __init__(self, fp, headers, params=None, parts=None): + # Make an instance-specific copy of the class processors + # so Tools, etc. can replace them per-request. + self.processors = self.processors.copy() + + self.fp = fp + self.headers = headers + + if params is None: + params = {} + self.params = params + + if parts is None: + parts = [] + self.parts = parts + + # Content-Type + self.content_type = headers.elements('Content-Type') + if self.content_type: + self.content_type = self.content_type[0] + else: + self.content_type = httputil.HeaderElement.from_str( + self.default_content_type) + + # Copy the class 'attempt_charsets', prepending any Content-Type + # charset + dec = self.content_type.params.get('charset', None) + if dec: + self.attempt_charsets = [dec] + [c for c in self.attempt_charsets + if c != dec] + else: + self.attempt_charsets = self.attempt_charsets[:] + + # Length + self.length = None + clen = headers.get('Content-Length', None) + # If Transfer-Encoding is 'chunked', ignore any Content-Length. + if ( + clen is not None and + 'chunked' not in headers.get('Transfer-Encoding', '') + ): + try: + self.length = int(clen) + except ValueError: + pass + + # Content-Disposition + self.name = None + self.filename = None + disp = headers.elements('Content-Disposition') + if disp: + disp = disp[0] + if 'name' in disp.params: + self.name = disp.params['name'] + if self.name.startswith('"') and self.name.endswith('"'): + self.name = self.name[1:-1] + if 'filename' in disp.params: + self.filename = disp.params['filename'] + if ( + self.filename.startswith('"') and + self.filename.endswith('"') + ): + self.filename = self.filename[1:-1] + if 'filename*' in disp.params: + # @see https://tools.ietf.org/html/rfc5987 + encoding, lang, filename = disp.params['filename*'].split("'") + self.filename = unquote(str(filename), encoding) + + def read(self, size=None, fp_out=None): + return self.fp.read(size, fp_out) + + def readline(self, size=None): + return self.fp.readline(size) + + def readlines(self, sizehint=None): + return self.fp.readlines(sizehint) + + def __iter__(self): + return self + + def __next__(self): + line = self.readline() + if not line: + raise StopIteration + return line + + def next(self): + return self.__next__() + + def read_into_file(self, fp_out=None): + """Read the request body into fp_out (or make_file() if None). + + Return fp_out. + """ + if fp_out is None: + fp_out = self.make_file() + self.read(fp_out=fp_out) + return fp_out + + def make_file(self): + """Return a file-like object into which the request body will be read. + + By default, this will return a TemporaryFile. Override as needed. + See also :attr:`cherrypy._cpreqbody.Part.maxrambytes`.""" + return tempfile.TemporaryFile() + + def fullvalue(self): + """Return this entity as a string, whether stored in a file or not.""" + if self.file: + # It was stored in a tempfile. Read it. + self.file.seek(0) + value = self.file.read() + self.file.seek(0) + else: + value = self.value + value = self.decode_entity(value) + return value + + def decode_entity(self, value): + """Return a given byte encoded value as a string""" + for charset in self.attempt_charsets: + try: + value = value.decode(charset) + except UnicodeDecodeError: + pass + else: + self.charset = charset + return value + else: + raise cherrypy.HTTPError( + 400, + 'The request entity could not be decoded. The following ' + 'charsets were attempted: %s' % repr(self.attempt_charsets) + ) + + def process(self): + """Execute the best-match processor for the given media type.""" + proc = None + ct = self.content_type.value + try: + proc = self.processors[ct] + except KeyError: + toptype = ct.split('/', 1)[0] + try: + proc = self.processors[toptype] + except KeyError: + pass + if proc is None: + self.default_proc() + else: + proc(self) + + def default_proc(self): + """Called if a more-specific processor is not found for the + ``Content-Type``. + """ + # Leave the fp alone for someone else to read. This works fine + # for request.body, but the Part subclasses need to override this + # so they can move on to the next part. + pass + + +class Part(Entity): + + """A MIME part entity, part of a multipart entity.""" + + # "The default character set, which must be assumed in the absence of a + # charset parameter, is US-ASCII." + attempt_charsets = ['us-ascii', 'utf-8'] + r"""A list of strings, each of which should be a known encoding. + + When the Content-Type of the request body warrants it, each of the given + encodings will be tried in order. The first one to successfully decode the + entity without raising an error is stored as + :attr:`entity.charset`. This defaults + to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by + `HTTP/1.1 + `_), + but ``['us-ascii', 'utf-8']`` for multipart parts. + """ + + boundary = None + """The MIME multipart boundary.""" + + default_content_type = 'text/plain' + """This defines a default ``Content-Type`` to use if no Content-Type header + is given. The empty string is used for RequestBody, which results in the + request body not being read or parsed at all. This is by design; a missing + ``Content-Type`` header in the HTTP request entity is an error at best, + and a security hole at worst. For multipart parts, however (this class), + the MIME spec declares that a part with no Content-Type defaults to + "text/plain". + """ + + # This is the default in stdlib cgi. We may want to increase it. + maxrambytes = 1000 + """The threshold of bytes after which point the ``Part`` will store + its data in a file (generated by + :func:`make_file`) + instead of a string. Defaults to 1000, just like the :mod:`cgi` + module in Python's standard library. + """ + + def __init__(self, fp, headers, boundary): + Entity.__init__(self, fp, headers) + self.boundary = boundary + self.file = None + self.value = None + + @classmethod + def from_fp(cls, fp, boundary): + headers = cls.read_headers(fp) + return cls(fp, headers, boundary) + + @classmethod + def read_headers(cls, fp): + headers = httputil.HeaderMap() + while True: + line = fp.readline() + if not line: + # No more data--illegal end of headers + raise EOFError('Illegal end of headers.') + + if line == b'\r\n': + # Normal end of headers + break + if not line.endswith(b'\r\n'): + raise ValueError('MIME requires CRLF terminators: %r' % line) + + if line[0] in b' \t': + # It's a continuation line. + v = line.strip().decode('ISO-8859-1') + else: + k, v = line.split(b':', 1) + k = k.strip().decode('ISO-8859-1') + v = v.strip().decode('ISO-8859-1') + + existing = headers.get(k) + if existing: + v = ', '.join((existing, v)) + headers[k] = v + + return headers + + def read_lines_to_boundary(self, fp_out=None): + """Read bytes from self.fp and return or write them to a file. + + If the 'fp_out' argument is None (the default), all bytes read are + returned in a single byte string. + + If the 'fp_out' argument is not None, it must be a file-like + object that supports the 'write' method; all bytes read will be + written to the fp, and that fp is returned. + """ + endmarker = self.boundary + b'--' + delim = b'' + prev_lf = True + lines = [] + seen = 0 + while True: + line = self.fp.readline(1 << 16) + if not line: + raise EOFError('Illegal end of multipart body.') + if line.startswith(b'--') and prev_lf: + strippedline = line.strip() + if strippedline == self.boundary: + break + if strippedline == endmarker: + self.fp.finish() + break + + line = delim + line + + if line.endswith(b'\r\n'): + delim = b'\r\n' + line = line[:-2] + prev_lf = True + elif line.endswith(b'\n'): + delim = b'\n' + line = line[:-1] + prev_lf = True + else: + delim = b'' + prev_lf = False + + if fp_out is None: + lines.append(line) + seen += len(line) + if seen > self.maxrambytes: + fp_out = self.make_file() + for line in lines: + fp_out.write(line) + else: + fp_out.write(line) + + if fp_out is None: + result = b''.join(lines) + return result + else: + fp_out.seek(0) + return fp_out + + def default_proc(self): + """Called if a more-specific processor is not found for the + ``Content-Type``. + """ + if self.filename: + # Always read into a file if a .filename was given. + self.file = self.read_into_file() + else: + result = self.read_lines_to_boundary() + if isinstance(result, bytes): + self.value = result + else: + self.file = result + + def read_into_file(self, fp_out=None): + """Read the request body into fp_out (or make_file() if None). + + Return fp_out. + """ + if fp_out is None: + fp_out = self.make_file() + self.read_lines_to_boundary(fp_out=fp_out) + return fp_out + + +Entity.part_class = Part + +inf = float('inf') + + +class SizedReader: + + def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE, + has_trailers=False): + # Wrap our fp in a buffer so peek() works + self.fp = fp + self.length = length + self.maxbytes = maxbytes + self.buffer = b'' + self.bufsize = bufsize + self.bytes_read = 0 + self.done = False + self.has_trailers = has_trailers + + def read(self, size=None, fp_out=None): + """Read bytes from the request body and return or write them to a file. + + A number of bytes less than or equal to the 'size' argument are read + off the socket. The actual number of bytes read are tracked in + self.bytes_read. The number may be smaller than 'size' when 1) the + client sends fewer bytes, 2) the 'Content-Length' request header + specifies fewer bytes than requested, or 3) the number of bytes read + exceeds self.maxbytes (in which case, 413 is raised). + + If the 'fp_out' argument is None (the default), all bytes read are + returned in a single byte string. + + If the 'fp_out' argument is not None, it must be a file-like + object that supports the 'write' method; all bytes read will be + written to the fp, and None is returned. + """ + + if self.length is None: + if size is None: + remaining = inf + else: + remaining = size + else: + remaining = self.length - self.bytes_read + if size and size < remaining: + remaining = size + if remaining == 0: + self.finish() + if fp_out is None: + return b'' + else: + return None + + chunks = [] + + # Read bytes from the buffer. + if self.buffer: + if remaining is inf: + data = self.buffer + self.buffer = b'' + else: + data = self.buffer[:remaining] + self.buffer = self.buffer[remaining:] + datalen = len(data) + remaining -= datalen + + # Check lengths. + self.bytes_read += datalen + if self.maxbytes and self.bytes_read > self.maxbytes: + raise cherrypy.HTTPError(413) + + # Store the data. + if fp_out is None: + chunks.append(data) + else: + fp_out.write(data) + + # Read bytes from the socket. + while remaining > 0: + chunksize = min(remaining, self.bufsize) + try: + data = self.fp.read(chunksize) + except Exception: + e = sys.exc_info()[1] + if e.__class__.__name__ == 'MaxSizeExceeded': + # Post data is too big + raise cherrypy.HTTPError( + 413, 'Maximum request length: %r' % e.args[1]) + else: + raise + if not data: + self.finish() + break + datalen = len(data) + remaining -= datalen + + # Check lengths. + self.bytes_read += datalen + if self.maxbytes and self.bytes_read > self.maxbytes: + raise cherrypy.HTTPError(413) + + # Store the data. + if fp_out is None: + chunks.append(data) + else: + fp_out.write(data) + + if fp_out is None: + return b''.join(chunks) + + def readline(self, size=None): + """Read a line from the request body and return it.""" + chunks = [] + while size is None or size > 0: + chunksize = self.bufsize + if size is not None and size < self.bufsize: + chunksize = size + data = self.read(chunksize) + if not data: + break + pos = data.find(b'\n') + 1 + if pos: + chunks.append(data[:pos]) + remainder = data[pos:] + self.buffer += remainder + self.bytes_read -= len(remainder) + break + else: + chunks.append(data) + return b''.join(chunks) + + def readlines(self, sizehint=None): + """Read lines from the request body and return them.""" + if self.length is not None: + if sizehint is None: + sizehint = self.length - self.bytes_read + else: + sizehint = min(sizehint, self.length - self.bytes_read) + + lines = [] + seen = 0 + while True: + line = self.readline() + if not line: + break + lines.append(line) + seen += len(line) + if seen >= sizehint: + break + return lines + + def finish(self): + self.done = True + if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'): + self.trailers = {} + + try: + for line in self.fp.read_trailer_lines(): + if line[0] in b' \t': + # It's a continuation line. + v = line.strip() + else: + try: + k, v = line.split(b':', 1) + except ValueError: + raise ValueError('Illegal header line.') + k = k.strip().title() + v = v.strip() + + if k in cheroot.server.comma_separated_headers: + existing = self.trailers.get(k) + if existing: + v = b', '.join((existing, v)) + self.trailers[k] = v + except Exception: + e = sys.exc_info()[1] + if e.__class__.__name__ == 'MaxSizeExceeded': + # Post data is too big + raise cherrypy.HTTPError( + 413, 'Maximum request length: %r' % e.args[1]) + else: + raise + + +class RequestBody(Entity): + + """The entity of the HTTP request.""" + + bufsize = 8 * 1024 + """The buffer size used when reading the socket.""" + + # Don't parse the request body at all if the client didn't provide + # a Content-Type header. See + # https://github.com/cherrypy/cherrypy/issues/790 + default_content_type = '' + """This defines a default ``Content-Type`` to use if no Content-Type header + is given. The empty string is used for RequestBody, which results in the + request body not being read or parsed at all. This is by design; a missing + ``Content-Type`` header in the HTTP request entity is an error at best, + and a security hole at worst. For multipart parts, however, the MIME spec + declares that a part with no Content-Type defaults to "text/plain" + (see :class:`Part`). + """ + + maxbytes = None + """Raise ``MaxSizeExceeded`` if more bytes than this are read from + the socket. + """ + + def __init__(self, fp, headers, params=None, request_params=None): + Entity.__init__(self, fp, headers, params) + + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1 + # When no explicit charset parameter is provided by the + # sender, media subtypes of the "text" type are defined + # to have a default charset value of "ISO-8859-1" when + # received via HTTP. + if self.content_type.value.startswith('text/'): + for c in ('ISO-8859-1', 'iso-8859-1', 'Latin-1', 'latin-1'): + if c in self.attempt_charsets: + break + else: + self.attempt_charsets.append('ISO-8859-1') + + # Temporary fix while deprecating passing .parts as .params. + self.processors['multipart'] = _old_process_multipart + + if request_params is None: + request_params = {} + self.request_params = request_params + + def process(self): + """Process the request entity based on its Content-Type.""" + # "The presence of a message-body in a request is signaled by the + # inclusion of a Content-Length or Transfer-Encoding header field in + # the request's message-headers." + # It is possible to send a POST request with no body, for example; + # however, app developers are responsible in that case to set + # cherrypy.request.process_body to False so this method isn't called. + h = cherrypy.serving.request.headers + if 'Content-Length' not in h and 'Transfer-Encoding' not in h: + raise cherrypy.HTTPError(411) + + self.fp = SizedReader(self.fp, self.length, + self.maxbytes, bufsize=self.bufsize, + has_trailers='Trailer' in h) + super(RequestBody, self).process() + + # Body params should also be a part of the request_params + # add them in here. + request_params = self.request_params + for key, value in self.params.items(): + # Python 2 only: keyword arguments must be byte strings (type + # 'str'). + if sys.version_info < (3, 0): + if isinstance(key, six.text_type): + key = key.encode('ISO-8859-1') + + if key in request_params: + if not isinstance(request_params[key], list): + request_params[key] = [request_params[key]] + request_params[key].append(value) + else: + request_params[key] = value diff --git a/resources/lib/cherrypy/_cprequest.py b/resources/lib/cherrypy/_cprequest.py new file mode 100644 index 0000000..3cc0c81 --- /dev/null +++ b/resources/lib/cherrypy/_cprequest.py @@ -0,0 +1,930 @@ +import sys +import time + +import uuid + +import six +from six.moves.http_cookies import SimpleCookie, CookieError + +from more_itertools import consume + +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy import _cpreqbody +from cherrypy._cperror import format_exc, bare_error +from cherrypy.lib import httputil, reprconf, encoding + + +class Hook(object): + + """A callback and its metadata: failsafe, priority, and kwargs.""" + + callback = None + """ + The bare callable that this Hook object is wrapping, which will + be called when the Hook is called.""" + + failsafe = False + """ + If True, the callback is guaranteed to run even if other callbacks + from the same call point raise exceptions.""" + + priority = 50 + """ + Defines the order of execution for a list of Hooks. Priority numbers + should be limited to the closed interval [0, 100], but values outside + this range are acceptable, as are fractional values.""" + + kwargs = {} + """ + A set of keyword arguments that will be passed to the + callable on each call.""" + + def __init__(self, callback, failsafe=None, priority=None, **kwargs): + self.callback = callback + + if failsafe is None: + failsafe = getattr(callback, 'failsafe', False) + self.failsafe = failsafe + + if priority is None: + priority = getattr(callback, 'priority', 50) + self.priority = priority + + self.kwargs = kwargs + + def __lt__(self, other): + """ + Hooks sort by priority, ascending, such that + hooks of lower priority are run first. + """ + return self.priority < other.priority + + def __call__(self): + """Run self.callback(**self.kwargs).""" + return self.callback(**self.kwargs) + + def __repr__(self): + cls = self.__class__ + return ('%s.%s(callback=%r, failsafe=%r, priority=%r, %s)' + % (cls.__module__, cls.__name__, self.callback, + self.failsafe, self.priority, + ', '.join(['%s=%r' % (k, v) + for k, v in self.kwargs.items()]))) + + +class HookMap(dict): + + """A map of call points to lists of callbacks (Hook objects).""" + + def __new__(cls, points=None): + d = dict.__new__(cls) + for p in points or []: + d[p] = [] + return d + + def __init__(self, *a, **kw): + pass + + def attach(self, point, callback, failsafe=None, priority=None, **kwargs): + """Append a new Hook made from the supplied arguments.""" + self[point].append(Hook(callback, failsafe, priority, **kwargs)) + + def run(self, point): + """Execute all registered Hooks (callbacks) for the given point.""" + exc = None + hooks = self[point] + hooks.sort() + for hook in hooks: + # Some hooks are guaranteed to run even if others at + # the same hookpoint fail. We will still log the failure, + # but proceed on to the next hook. The only way + # to stop all processing from one of these hooks is + # to raise SystemExit and stop the whole server. + if exc is None or hook.failsafe: + try: + hook() + except (KeyboardInterrupt, SystemExit): + raise + except (cherrypy.HTTPError, cherrypy.HTTPRedirect, + cherrypy.InternalRedirect): + exc = sys.exc_info()[1] + except Exception: + exc = sys.exc_info()[1] + cherrypy.log(traceback=True, severity=40) + if exc: + raise exc + + def __copy__(self): + newmap = self.__class__() + # We can't just use 'update' because we want copies of the + # mutable values (each is a list) as well. + for k, v in self.items(): + newmap[k] = v[:] + return newmap + copy = __copy__ + + def __repr__(self): + cls = self.__class__ + return '%s.%s(points=%r)' % ( + cls.__module__, + cls.__name__, + list(self) + ) + + +# Config namespace handlers + +def hooks_namespace(k, v): + """Attach bare hooks declared in config.""" + # Use split again to allow multiple hooks for a single + # hookpoint per path (e.g. "hooks.before_handler.1"). + # Little-known fact you only get from reading source ;) + hookpoint = k.split('.', 1)[0] + if isinstance(v, six.string_types): + v = cherrypy.lib.reprconf.attributes(v) + if not isinstance(v, Hook): + v = Hook(v) + cherrypy.serving.request.hooks[hookpoint].append(v) + + +def request_namespace(k, v): + """Attach request attributes declared in config.""" + # Provides config entries to set request.body attrs (like + # attempt_charsets). + if k[:5] == 'body.': + setattr(cherrypy.serving.request.body, k[5:], v) + else: + setattr(cherrypy.serving.request, k, v) + + +def response_namespace(k, v): + """Attach response attributes declared in config.""" + # Provides config entries to set default response headers + # http://cherrypy.org/ticket/889 + if k[:8] == 'headers.': + cherrypy.serving.response.headers[k.split('.', 1)[1]] = v + else: + setattr(cherrypy.serving.response, k, v) + + +def error_page_namespace(k, v): + """Attach error pages declared in config.""" + if k != 'default': + k = int(k) + cherrypy.serving.request.error_page[k] = v + + +hookpoints = ['on_start_resource', 'before_request_body', + 'before_handler', 'before_finalize', + 'on_end_resource', 'on_end_request', + 'before_error_response', 'after_error_response'] + + +class Request(object): + + """An HTTP request. + + This object represents the metadata of an HTTP request message; + that is, it contains attributes which describe the environment + in which the request URL, headers, and body were sent (if you + want tools to interpret the headers and body, those are elsewhere, + mostly in Tools). This 'metadata' consists of socket data, + transport characteristics, and the Request-Line. This object + also contains data regarding the configuration in effect for + the given URL, and the execution plan for generating a response. + """ + + prev = None + """ + The previous Request object (if any). This should be None + unless we are processing an InternalRedirect.""" + + # Conversation/connection attributes + local = httputil.Host('127.0.0.1', 80) + 'An httputil.Host(ip, port, hostname) object for the server socket.' + + remote = httputil.Host('127.0.0.1', 1111) + 'An httputil.Host(ip, port, hostname) object for the client socket.' + + scheme = 'http' + """ + The protocol used between client and server. In most cases, + this will be either 'http' or 'https'.""" + + server_protocol = 'HTTP/1.1' + """ + The HTTP version for which the HTTP server is at least + conditionally compliant.""" + + base = '' + """The (scheme://host) portion of the requested URL. + In some cases (e.g. when proxying via mod_rewrite), this may contain + path segments which cherrypy.url uses when constructing url's, but + which otherwise are ignored by CherryPy. Regardless, this value + MUST NOT end in a slash.""" + + # Request-Line attributes + request_line = '' + """ + The complete Request-Line received from the client. This is a + single string consisting of the request method, URI, and protocol + version (joined by spaces). Any final CRLF is removed.""" + + method = 'GET' + """ + Indicates the HTTP method to be performed on the resource identified + by the Request-URI. Common methods include GET, HEAD, POST, PUT, and + DELETE. CherryPy allows any extension method; however, various HTTP + servers and gateways may restrict the set of allowable methods. + CherryPy applications SHOULD restrict the set (on a per-URI basis).""" + + query_string = '' + """ + The query component of the Request-URI, a string of information to be + interpreted by the resource. The query portion of a URI follows the + path component, and is separated by a '?'. For example, the URI + 'http://www.cherrypy.org/wiki?a=3&b=4' has the query component, + 'a=3&b=4'.""" + + query_string_encoding = 'utf8' + """ + The encoding expected for query string arguments after % HEX HEX decoding). + If a query string is provided that cannot be decoded with this encoding, + 404 is raised (since technically it's a different URI). If you want + arbitrary encodings to not error, set this to 'Latin-1'; you can then + encode back to bytes and re-decode to whatever encoding you like later. + """ + + protocol = (1, 1) + """The HTTP protocol version corresponding to the set + of features which should be allowed in the response. If BOTH + the client's request message AND the server's level of HTTP + compliance is HTTP/1.1, this attribute will be the tuple (1, 1). + If either is 1.0, this attribute will be the tuple (1, 0). + Lower HTTP protocol versions are not explicitly supported.""" + + params = {} + """ + A dict which combines query string (GET) and request entity (POST) + variables. This is populated in two stages: GET params are added + before the 'on_start_resource' hook, and POST params are added + between the 'before_request_body' and 'before_handler' hooks.""" + + # Message attributes + header_list = [] + """ + A list of the HTTP request headers as (name, value) tuples. + In general, you should use request.headers (a dict) instead.""" + + headers = httputil.HeaderMap() + """ + A dict-like object containing the request headers. Keys are header + names (in Title-Case format); however, you may get and set them in + a case-insensitive manner. That is, headers['Content-Type'] and + headers['content-type'] refer to the same value. Values are header + values (decoded according to :rfc:`2047` if necessary). See also: + httputil.HeaderMap, httputil.HeaderElement.""" + + cookie = SimpleCookie() + """See help(Cookie).""" + + rfile = None + """ + If the request included an entity (body), it will be available + as a stream in this attribute. However, the rfile will normally + be read for you between the 'before_request_body' hook and the + 'before_handler' hook, and the resulting string is placed into + either request.params or the request.body attribute. + + You may disable the automatic consumption of the rfile by setting + request.process_request_body to False, either in config for the desired + path, or in an 'on_start_resource' or 'before_request_body' hook. + + WARNING: In almost every case, you should not attempt to read from the + rfile stream after CherryPy's automatic mechanism has read it. If you + turn off the automatic parsing of rfile, you should read exactly the + number of bytes specified in request.headers['Content-Length']. + Ignoring either of these warnings may result in a hung request thread + or in corruption of the next (pipelined) request. + """ + + process_request_body = True + """ + If True, the rfile (if any) is automatically read and parsed, + and the result placed into request.params or request.body.""" + + methods_with_bodies = ('POST', 'PUT', 'PATCH') + """ + A sequence of HTTP methods for which CherryPy will automatically + attempt to read a body from the rfile. If you are going to change + this property, modify it on the configuration (recommended) + or on the "hook point" `on_start_resource`. + """ + + body = None + """ + If the request Content-Type is 'application/x-www-form-urlencoded' + or multipart, this will be None. Otherwise, this will be an instance + of :class:`RequestBody` (which you + can .read()); this value is set between the 'before_request_body' and + 'before_handler' hooks (assuming that process_request_body is True).""" + + # Dispatch attributes + dispatch = cherrypy.dispatch.Dispatcher() + """ + The object which looks up the 'page handler' callable and collects + config for the current request based on the path_info, other + request attributes, and the application architecture. The core + calls the dispatcher as early as possible, passing it a 'path_info' + argument. + + The default dispatcher discovers the page handler by matching path_info + to a hierarchical arrangement of objects, starting at request.app.root. + See help(cherrypy.dispatch) for more information.""" + + script_name = '' + """ + The 'mount point' of the application which is handling this request. + + This attribute MUST NOT end in a slash. If the script_name refers to + the root of the URI, it MUST be an empty string (not "/"). + """ + + path_info = '/' + """ + The 'relative path' portion of the Request-URI. This is relative + to the script_name ('mount point') of the application which is + handling this request.""" + + login = None + """ + When authentication is used during the request processing this is + set to 'False' if it failed and to the 'username' value if it succeeded. + The default 'None' implies that no authentication happened.""" + + # Note that cherrypy.url uses "if request.app:" to determine whether + # the call is during a real HTTP request or not. So leave this None. + app = None + """The cherrypy.Application object which is handling this request.""" + + handler = None + """ + The function, method, or other callable which CherryPy will call to + produce the response. The discovery of the handler and the arguments + it will receive are determined by the request.dispatch object. + By default, the handler is discovered by walking a tree of objects + starting at request.app.root, and is then passed all HTTP params + (from the query string and POST body) as keyword arguments.""" + + toolmaps = {} + """ + A nested dict of all Toolboxes and Tools in effect for this request, + of the form: {Toolbox.namespace: {Tool.name: config dict}}.""" + + config = None + """ + A flat dict of all configuration entries which apply to the + current request. These entries are collected from global config, + application config (based on request.path_info), and from handler + config (exactly how is governed by the request.dispatch object in + effect for this request; by default, handler config can be attached + anywhere in the tree between request.app.root and the final handler, + and inherits downward).""" + + is_index = None + """ + This will be True if the current request is mapped to an 'index' + resource handler (also, a 'default' handler if path_info ends with + a slash). The value may be used to automatically redirect the + user-agent to a 'more canonical' URL which either adds or removes + the trailing slash. See cherrypy.tools.trailing_slash.""" + + hooks = HookMap(hookpoints) + """ + A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}. + Each key is a str naming the hook point, and each value is a list + of hooks which will be called at that hook point during this request. + The list of hooks is generally populated as early as possible (mostly + from Tools specified in config), but may be extended at any time. + See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools.""" + + error_response = cherrypy.HTTPError(500).set_response + """ + The no-arg callable which will handle unexpected, untrapped errors + during request processing. This is not used for expected exceptions + (like NotFound, HTTPError, or HTTPRedirect) which are raised in + response to expected conditions (those should be customized either + via request.error_page or by overriding HTTPError.set_response). + By default, error_response uses HTTPError(500) to return a generic + error response to the user-agent.""" + + error_page = {} + """ + A dict of {error code: response filename or callable} pairs. + + The error code must be an int representing a given HTTP error code, + or the string 'default', which will be used if no matching entry + is found for a given numeric code. + + If a filename is provided, the file should contain a Python string- + formatting template, and can expect by default to receive format + values with the mapping keys %(status)s, %(message)s, %(traceback)s, + and %(version)s. The set of format mappings can be extended by + overriding HTTPError.set_response. + + If a callable is provided, it will be called by default with keyword + arguments 'status', 'message', 'traceback', and 'version', as for a + string-formatting template. The callable must return a string or + iterable of strings which will be set to response.body. It may also + override headers or perform any other processing. + + If no entry is given for an error code, and no 'default' entry exists, + a default template will be used. + """ + + show_tracebacks = True + """ + If True, unexpected errors encountered during request processing will + include a traceback in the response body.""" + + show_mismatched_params = True + """ + If True, mismatched parameters encountered during PageHandler invocation + processing will be included in the response body.""" + + throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect) + """The sequence of exceptions which Request.run does not trap.""" + + throw_errors = False + """ + If True, Request.run will not trap any errors (except HTTPRedirect and + HTTPError, which are more properly called 'exceptions', not errors).""" + + closed = False + """True once the close method has been called, False otherwise.""" + + stage = None + """ + A string containing the stage reached in the request-handling process. + This is useful when debugging a live server with hung requests.""" + + unique_id = None + """A lazy object generating and memorizing UUID4 on ``str()`` render.""" + + namespaces = reprconf.NamespaceSet( + **{'hooks': hooks_namespace, + 'request': request_namespace, + 'response': response_namespace, + 'error_page': error_page_namespace, + 'tools': cherrypy.tools, + }) + + def __init__(self, local_host, remote_host, scheme='http', + server_protocol='HTTP/1.1'): + """Populate a new Request object. + + local_host should be an httputil.Host object with the server info. + remote_host should be an httputil.Host object with the client info. + scheme should be a string, either "http" or "https". + """ + self.local = local_host + self.remote = remote_host + self.scheme = scheme + self.server_protocol = server_protocol + + self.closed = False + + # Put a *copy* of the class error_page into self. + self.error_page = self.error_page.copy() + + # Put a *copy* of the class namespaces into self. + self.namespaces = self.namespaces.copy() + + self.stage = None + + self.unique_id = LazyUUID4() + + def close(self): + """Run cleanup code. (Core)""" + if not self.closed: + self.closed = True + self.stage = 'on_end_request' + self.hooks.run('on_end_request') + self.stage = 'close' + + def run(self, method, path, query_string, req_protocol, headers, rfile): + r"""Process the Request. (Core) + + method, path, query_string, and req_protocol should be pulled directly + from the Request-Line (e.g. "GET /path?key=val HTTP/1.0"). + + path + This should be %XX-unquoted, but query_string should not be. + + When using Python 2, they both MUST be byte strings, + not unicode strings. + + When using Python 3, they both MUST be unicode strings, + not byte strings, and preferably not bytes \x00-\xFF + disguised as unicode. + + headers + A list of (name, value) tuples. + + rfile + A file-like object containing the HTTP request entity. + + When run() is done, the returned object should have 3 attributes: + + * status, e.g. "200 OK" + * header_list, a list of (name, value) tuples + * body, an iterable yielding strings + + Consumer code (HTTP servers) should then access these response + attributes to build the outbound stream. + + """ + response = cherrypy.serving.response + self.stage = 'run' + try: + self.error_response = cherrypy.HTTPError(500).set_response + + self.method = method + path = path or '/' + self.query_string = query_string or '' + self.params = {} + + # Compare request and server HTTP protocol versions, in case our + # server does not support the requested protocol. Limit our output + # to min(req, server). We want the following output: + # request server actual written supported response + # protocol protocol response protocol feature set + # a 1.0 1.0 1.0 1.0 + # b 1.0 1.1 1.1 1.0 + # c 1.1 1.0 1.0 1.0 + # d 1.1 1.1 1.1 1.1 + # Notice that, in (b), the response will be "HTTP/1.1" even though + # the client only understands 1.0. RFC 2616 10.5.6 says we should + # only return 505 if the _major_ version is different. + rp = int(req_protocol[5]), int(req_protocol[7]) + sp = int(self.server_protocol[5]), int(self.server_protocol[7]) + self.protocol = min(rp, sp) + response.headers.protocol = self.protocol + + # Rebuild first line of the request (e.g. "GET /path HTTP/1.0"). + url = path + if query_string: + url += '?' + query_string + self.request_line = '%s %s %s' % (method, url, req_protocol) + + self.header_list = list(headers) + self.headers = httputil.HeaderMap() + + self.rfile = rfile + self.body = None + + self.cookie = SimpleCookie() + self.handler = None + + # path_info should be the path from the + # app root (script_name) to the handler. + self.script_name = self.app.script_name + self.path_info = pi = path[len(self.script_name):] + + self.stage = 'respond' + self.respond(pi) + + except self.throws: + raise + except Exception: + if self.throw_errors: + raise + else: + # Failure in setup, error handler or finalize. Bypass them. + # Can't use handle_error because we may not have hooks yet. + cherrypy.log(traceback=True, severity=40) + if self.show_tracebacks: + body = format_exc() + else: + body = '' + r = bare_error(body) + response.output_status, response.header_list, response.body = r + + if self.method == 'HEAD': + # HEAD requests MUST NOT return a message-body in the response. + response.body = [] + + try: + cherrypy.log.access() + except Exception: + cherrypy.log.error(traceback=True) + + return response + + def respond(self, path_info): + """Generate a response for the resource at self.path_info. (Core)""" + try: + try: + try: + self._do_respond(path_info) + except (cherrypy.HTTPRedirect, cherrypy.HTTPError): + inst = sys.exc_info()[1] + inst.set_response() + self.stage = 'before_finalize (HTTPError)' + self.hooks.run('before_finalize') + cherrypy.serving.response.finalize() + finally: + self.stage = 'on_end_resource' + self.hooks.run('on_end_resource') + except self.throws: + raise + except Exception: + if self.throw_errors: + raise + self.handle_error() + + def _do_respond(self, path_info): + response = cherrypy.serving.response + + if self.app is None: + raise cherrypy.NotFound() + + self.hooks = self.__class__.hooks.copy() + self.toolmaps = {} + + # Get the 'Host' header, so we can HTTPRedirect properly. + self.stage = 'process_headers' + self.process_headers() + + self.stage = 'get_resource' + self.get_resource(path_info) + + self.body = _cpreqbody.RequestBody( + self.rfile, self.headers, request_params=self.params) + + self.namespaces(self.config) + + self.stage = 'on_start_resource' + self.hooks.run('on_start_resource') + + # Parse the querystring + self.stage = 'process_query_string' + self.process_query_string() + + # Process the body + if self.process_request_body: + if self.method not in self.methods_with_bodies: + self.process_request_body = False + self.stage = 'before_request_body' + self.hooks.run('before_request_body') + if self.process_request_body: + self.body.process() + + # Run the handler + self.stage = 'before_handler' + self.hooks.run('before_handler') + if self.handler: + self.stage = 'handler' + response.body = self.handler() + + # Finalize + self.stage = 'before_finalize' + self.hooks.run('before_finalize') + response.finalize() + + def process_query_string(self): + """Parse the query string into Python structures. (Core)""" + try: + p = httputil.parse_query_string( + self.query_string, encoding=self.query_string_encoding) + except UnicodeDecodeError: + raise cherrypy.HTTPError( + 404, 'The given query string could not be processed. Query ' + 'strings for this resource must be encoded with %r.' % + self.query_string_encoding) + + # Python 2 only: keyword arguments must be byte strings (type 'str'). + if six.PY2: + for key, value in p.items(): + if isinstance(key, six.text_type): + del p[key] + p[key.encode(self.query_string_encoding)] = value + self.params.update(p) + + def process_headers(self): + """Parse HTTP header data into Python structures. (Core)""" + # Process the headers into self.headers + headers = self.headers + for name, value in self.header_list: + # Call title() now (and use dict.__method__(headers)) + # so title doesn't have to be called twice. + name = name.title() + value = value.strip() + + headers[name] = httputil.decode_TEXT_maybe(value) + + # Some clients, notably Konquoror, supply multiple + # cookies on different lines with the same key. To + # handle this case, store all cookies in self.cookie. + if name == 'Cookie': + try: + self.cookie.load(value) + except CookieError as exc: + raise cherrypy.HTTPError(400, str(exc)) + + if not dict.__contains__(headers, 'Host'): + # All Internet-based HTTP/1.1 servers MUST respond with a 400 + # (Bad Request) status code to any HTTP/1.1 request message + # which lacks a Host header field. + if self.protocol >= (1, 1): + msg = "HTTP/1.1 requires a 'Host' request header." + raise cherrypy.HTTPError(400, msg) + host = dict.get(headers, 'Host') + if not host: + host = self.local.name or self.local.ip + self.base = '%s://%s' % (self.scheme, host) + + def get_resource(self, path): + """Call a dispatcher (which sets self.handler and .config). (Core)""" + # First, see if there is a custom dispatch at this URI. Custom + # dispatchers can only be specified in app.config, not in _cp_config + # (since custom dispatchers may not even have an app.root). + dispatch = self.app.find_config( + path, 'request.dispatch', self.dispatch) + + # dispatch() should set self.handler and self.config + dispatch(path) + + def handle_error(self): + """Handle the last unanticipated exception. (Core)""" + try: + self.hooks.run('before_error_response') + if self.error_response: + self.error_response() + self.hooks.run('after_error_response') + cherrypy.serving.response.finalize() + except cherrypy.HTTPRedirect: + inst = sys.exc_info()[1] + inst.set_response() + cherrypy.serving.response.finalize() + + +class ResponseBody(object): + + """The body of the HTTP response (the response entity).""" + + unicode_err = ('Page handlers MUST return bytes. Use tools.encode ' + 'if you wish to return unicode.') + + def __get__(self, obj, objclass=None): + if obj is None: + # When calling on the class instead of an instance... + return self + else: + return obj._body + + def __set__(self, obj, value): + # Convert the given value to an iterable object. + if isinstance(value, six.text_type): + raise ValueError(self.unicode_err) + elif isinstance(value, list): + # every item in a list must be bytes... + if any(isinstance(item, six.text_type) for item in value): + raise ValueError(self.unicode_err) + + obj._body = encoding.prepare_iter(value) + + +class Response(object): + + """An HTTP Response, including status, headers, and body.""" + + status = '' + """The HTTP Status-Code and Reason-Phrase.""" + + header_list = [] + """ + A list of the HTTP response headers as (name, value) tuples. + In general, you should use response.headers (a dict) instead. This + attribute is generated from response.headers and is not valid until + after the finalize phase.""" + + headers = httputil.HeaderMap() + """ + A dict-like object containing the response headers. Keys are header + names (in Title-Case format); however, you may get and set them in + a case-insensitive manner. That is, headers['Content-Type'] and + headers['content-type'] refer to the same value. Values are header + values (decoded according to :rfc:`2047` if necessary). + + .. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement` + """ + + cookie = SimpleCookie() + """See help(Cookie).""" + + body = ResponseBody() + """The body (entity) of the HTTP response.""" + + time = None + """The value of time.time() when created. Use in HTTP dates.""" + + stream = False + """If False, buffer the response body.""" + + def __init__(self): + self.status = None + self.header_list = None + self._body = [] + self.time = time.time() + + self.headers = httputil.HeaderMap() + # Since we know all our keys are titled strings, we can + # bypass HeaderMap.update and get a big speed boost. + dict.update(self.headers, { + 'Content-Type': 'text/html', + 'Server': 'CherryPy/' + cherrypy.__version__, + 'Date': httputil.HTTPDate(self.time), + }) + self.cookie = SimpleCookie() + + def collapse_body(self): + """Collapse self.body to a single string; replace it and return it.""" + new_body = b''.join(self.body) + self.body = new_body + return new_body + + def _flush_body(self): + """ + Discard self.body but consume any generator such that + any finalization can occur, such as is required by + caching.tee_output(). + """ + consume(iter(self.body)) + + def finalize(self): + """Transform headers (and cookies) into self.header_list. (Core)""" + try: + code, reason, _ = httputil.valid_status(self.status) + except ValueError: + raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0]) + + headers = self.headers + + self.status = '%s %s' % (code, reason) + self.output_status = ntob(str(code), 'ascii') + \ + b' ' + headers.encode(reason) + + if self.stream: + # The upshot: wsgiserver will chunk the response if + # you pop Content-Length (or set it explicitly to None). + # Note that lib.static sets C-L to the file's st_size. + if dict.get(headers, 'Content-Length') is None: + dict.pop(headers, 'Content-Length', None) + elif code < 200 or code in (204, 205, 304): + # "All 1xx (informational), 204 (no content), + # and 304 (not modified) responses MUST NOT + # include a message-body." + dict.pop(headers, 'Content-Length', None) + self._flush_body() + self.body = b'' + else: + # Responses which are not streamed should have a Content-Length, + # but allow user code to set Content-Length if desired. + if dict.get(headers, 'Content-Length') is None: + content = self.collapse_body() + dict.__setitem__(headers, 'Content-Length', len(content)) + + # Transform our header dict into a list of tuples. + self.header_list = h = headers.output() + + cookie = self.cookie.output() + if cookie: + for line in cookie.split('\r\n'): + name, value = line.split(': ', 1) + if isinstance(name, six.text_type): + name = name.encode('ISO-8859-1') + if isinstance(value, six.text_type): + value = headers.encode(value) + h.append((name, value)) + + +class LazyUUID4(object): + def __str__(self): + """Return UUID4 and keep it for future calls.""" + return str(self.uuid4) + + @property + def uuid4(self): + """Provide unique id on per-request basis using UUID4. + + It's evaluated lazily on render. + """ + try: + self._uuid4 + except AttributeError: + # evaluate on first access + self._uuid4 = uuid.uuid4() + + return self._uuid4 diff --git a/resources/lib/cherrypy/_cpserver.py b/resources/lib/cherrypy/_cpserver.py new file mode 100644 index 0000000..0f60e2c --- /dev/null +++ b/resources/lib/cherrypy/_cpserver.py @@ -0,0 +1,252 @@ +"""Manage HTTP servers with CherryPy.""" + +import six + +import cherrypy +from cherrypy.lib.reprconf import attributes +from cherrypy._cpcompat import text_or_bytes +from cherrypy.process.servers import ServerAdapter + + +__all__ = ('Server', ) + + +class Server(ServerAdapter): + """An adapter for an HTTP server. + + You can set attributes (like socket_host and socket_port) + on *this* object (which is probably cherrypy.server), and call + quickstart. For example:: + + cherrypy.server.socket_port = 80 + cherrypy.quickstart() + """ + + socket_port = 8080 + """The TCP port on which to listen for connections.""" + + _socket_host = '127.0.0.1' + + @property + def socket_host(self): # noqa: D401; irrelevant for properties + """The hostname or IP address on which to listen for connections. + + Host values may be any IPv4 or IPv6 address, or any valid hostname. + The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if + your hosts file prefers IPv6). The string '0.0.0.0' is a special + IPv4 entry meaning "any active interface" (INADDR_ANY), and '::' + is the similar IN6ADDR_ANY for IPv6. The empty string or None are + not allowed. + """ + return self._socket_host + + @socket_host.setter + def socket_host(self, value): + if value == '': + raise ValueError("The empty string ('') is not an allowed value. " + "Use '0.0.0.0' instead to listen on all active " + 'interfaces (INADDR_ANY).') + self._socket_host = value + + socket_file = None + """If given, the name of the UNIX socket to use instead of TCP/IP. + + When this option is not None, the `socket_host` and `socket_port` options + are ignored.""" + + socket_queue_size = 5 + """The 'backlog' argument to socket.listen(); specifies the maximum number + of queued connections (default 5).""" + + socket_timeout = 10 + """The timeout in seconds for accepted connections (default 10).""" + + accepted_queue_size = -1 + """The maximum number of requests which will be queued up before + the server refuses to accept it (default -1, meaning no limit).""" + + accepted_queue_timeout = 10 + """The timeout in seconds for attempting to add a request to the + queue when the queue is full (default 10).""" + + shutdown_timeout = 5 + """The time to wait for HTTP worker threads to clean up.""" + + protocol_version = 'HTTP/1.1' + """The version string to write in the Status-Line of all HTTP responses, + for example, "HTTP/1.1" (the default). Depending on the HTTP server used, + this should also limit the supported features used in the response.""" + + thread_pool = 10 + """The number of worker threads to start up in the pool.""" + + thread_pool_max = -1 + """The maximum size of the worker-thread pool. Use -1 to indicate no limit. + """ + + max_request_header_size = 500 * 1024 + """The maximum number of bytes allowable in the request headers. + If exceeded, the HTTP server should return "413 Request Entity Too Large". + """ + + max_request_body_size = 100 * 1024 * 1024 + """The maximum number of bytes allowable in the request body. If exceeded, + the HTTP server should return "413 Request Entity Too Large".""" + + instance = None + """If not None, this should be an HTTP server instance (such as + cheroot.wsgi.Server) which cherrypy.server will control. + Use this when you need + more control over object instantiation than is available in the various + configuration options.""" + + ssl_context = None + """When using PyOpenSSL, an instance of SSL.Context.""" + + ssl_certificate = None + """The filename of the SSL certificate to use.""" + + ssl_certificate_chain = None + """When using PyOpenSSL, the certificate chain to pass to + Context.load_verify_locations.""" + + ssl_private_key = None + """The filename of the private key to use with SSL.""" + + ssl_ciphers = None + """The ciphers list of SSL.""" + + if six.PY3: + ssl_module = 'builtin' + """The name of a registered SSL adaptation module to use with + the builtin WSGI server. Builtin options are: 'builtin' (to + use the SSL library built into recent versions of Python). + You may also register your own classes in the + cheroot.server.ssl_adapters dict.""" + else: + ssl_module = 'pyopenssl' + """The name of a registered SSL adaptation module to use with the + builtin WSGI server. Builtin options are 'builtin' (to use the SSL + library built into recent versions of Python) and 'pyopenssl' (to + use the PyOpenSSL project, which you must install separately). You + may also register your own classes in the cheroot.server.ssl_adapters + dict.""" + + statistics = False + """Turns statistics-gathering on or off for aware HTTP servers.""" + + nodelay = True + """If True (the default since 3.1), sets the TCP_NODELAY socket option.""" + + wsgi_version = (1, 0) + """The WSGI version tuple to use with the builtin WSGI server. + The provided options are (1, 0) [which includes support for PEP 3333, + which declares it covers WSGI version 1.0.1 but still mandates the + wsgi.version (1, 0)] and ('u', 0), an experimental unicode version. + You may create and register your own experimental versions of the WSGI + protocol by adding custom classes to the cheroot.server.wsgi_gateways dict. + """ + + peercreds = False + """If True, peer cred lookup for UNIX domain socket will put to WSGI env. + + This information will then be available through WSGI env vars: + * X_REMOTE_PID + * X_REMOTE_UID + * X_REMOTE_GID + """ + + peercreds_resolve = False + """If True, username/group will be looked up in the OS from peercreds. + + This information will then be available through WSGI env vars: + * REMOTE_USER + * X_REMOTE_USER + * X_REMOTE_GROUP + """ + + def __init__(self): + """Initialize Server instance.""" + self.bus = cherrypy.engine + self.httpserver = None + self.interrupt = None + self.running = False + + def httpserver_from_self(self, httpserver=None): + """Return a (httpserver, bind_addr) pair based on self attributes.""" + if httpserver is None: + httpserver = self.instance + if httpserver is None: + from cherrypy import _cpwsgi_server + httpserver = _cpwsgi_server.CPWSGIServer(self) + if isinstance(httpserver, text_or_bytes): + # Is anyone using this? Can I add an arg? + httpserver = attributes(httpserver)(self) + return httpserver, self.bind_addr + + def start(self): + """Start the HTTP server.""" + if not self.httpserver: + self.httpserver, self.bind_addr = self.httpserver_from_self() + super(Server, self).start() + start.priority = 75 + + @property + def bind_addr(self): + """Return bind address. + + A (host, port) tuple for TCP sockets or a str for Unix domain sockts. + """ + if self.socket_file: + return self.socket_file + if self.socket_host is None and self.socket_port is None: + return None + return (self.socket_host, self.socket_port) + + @bind_addr.setter + def bind_addr(self, value): + if value is None: + self.socket_file = None + self.socket_host = None + self.socket_port = None + elif isinstance(value, text_or_bytes): + self.socket_file = value + self.socket_host = None + self.socket_port = None + else: + try: + self.socket_host, self.socket_port = value + self.socket_file = None + except ValueError: + raise ValueError('bind_addr must be a (host, port) tuple ' + '(for TCP sockets) or a string (for Unix ' + 'domain sockets), not %r' % value) + + def base(self): + """Return the base for this server. + + e.i. scheme://host[:port] or sock file + """ + if self.socket_file: + return self.socket_file + + host = self.socket_host + if host in ('0.0.0.0', '::'): + # 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY. + # Look up the host name, which should be the + # safest thing to spit out in a URL. + import socket + host = socket.gethostname() + + port = self.socket_port + + if self.ssl_certificate: + scheme = 'https' + if port != 443: + host += ':%s' % port + else: + scheme = 'http' + if port != 80: + host += ':%s' % port + + return '%s://%s' % (scheme, host) diff --git a/resources/lib/cherrypy/_cptools.py b/resources/lib/cherrypy/_cptools.py new file mode 100644 index 0000000..5746028 --- /dev/null +++ b/resources/lib/cherrypy/_cptools.py @@ -0,0 +1,509 @@ +"""CherryPy tools. A "tool" is any helper, adapted to CP. + +Tools are usually designed to be used in a variety of ways (although some +may only offer one if they choose): + + Library calls + All tools are callables that can be used wherever needed. + The arguments are straightforward and should be detailed within the + docstring. + + Function decorators + All tools, when called, may be used as decorators which configure + individual CherryPy page handlers (methods on the CherryPy tree). + That is, "@tools.anytool()" should "turn on" the tool via the + decorated function's _cp_config attribute. + + CherryPy config + If a tool exposes a "_setup" callable, it will be called + once per Request (if the feature is "turned on" via config). + +Tools may be implemented as any object with a namespace. The builtins +are generally either modules or instances of the tools.Tool class. +""" + +import six + +import cherrypy +from cherrypy._helper import expose + +from cherrypy.lib import cptools, encoding, static, jsontools +from cherrypy.lib import sessions as _sessions, xmlrpcutil as _xmlrpc +from cherrypy.lib import caching as _caching +from cherrypy.lib import auth_basic, auth_digest + + +def _getargs(func): + """Return the names of all static arguments to the given function.""" + # Use this instead of importing inspect for less mem overhead. + import types + if six.PY3: + if isinstance(func, types.MethodType): + func = func.__func__ + co = func.__code__ + else: + if isinstance(func, types.MethodType): + func = func.im_func + co = func.func_code + return co.co_varnames[:co.co_argcount] + + +_attr_error = ( + 'CherryPy Tools cannot be turned on directly. Instead, turn them ' + 'on via config, or use them as decorators on your page handlers.' +) + + +class Tool(object): + + """A registered function for use with CherryPy request-processing hooks. + + help(tool.callable) should give you more information about this Tool. + """ + + namespace = 'tools' + + def __init__(self, point, callable, name=None, priority=50): + self._point = point + self.callable = callable + self._name = name + self._priority = priority + self.__doc__ = self.callable.__doc__ + self._setargs() + + @property + def on(self): + raise AttributeError(_attr_error) + + @on.setter + def on(self, value): + raise AttributeError(_attr_error) + + def _setargs(self): + """Copy func parameter names to obj attributes.""" + try: + for arg in _getargs(self.callable): + setattr(self, arg, None) + except (TypeError, AttributeError): + if hasattr(self.callable, '__call__'): + for arg in _getargs(self.callable.__call__): + setattr(self, arg, None) + # IronPython 1.0 raises NotImplementedError because + # inspect.getargspec tries to access Python bytecode + # in co_code attribute. + except NotImplementedError: + pass + # IronPython 1B1 may raise IndexError in some cases, + # but if we trap it here it doesn't prevent CP from working. + except IndexError: + pass + + def _merged_args(self, d=None): + """Return a dict of configuration entries for this Tool.""" + if d: + conf = d.copy() + else: + conf = {} + + tm = cherrypy.serving.request.toolmaps[self.namespace] + if self._name in tm: + conf.update(tm[self._name]) + + if 'on' in conf: + del conf['on'] + + return conf + + def __call__(self, *args, **kwargs): + """Compile-time decorator (turn on the tool in config). + + For example:: + + @expose + @tools.proxy() + def whats_my_base(self): + return cherrypy.request.base + """ + if args: + raise TypeError('The %r Tool does not accept positional ' + 'arguments; you must use keyword arguments.' + % self._name) + + def tool_decorator(f): + if not hasattr(f, '_cp_config'): + f._cp_config = {} + subspace = self.namespace + '.' + self._name + '.' + f._cp_config[subspace + 'on'] = True + for k, v in kwargs.items(): + f._cp_config[subspace + k] = v + return f + return tool_decorator + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + conf = self._merged_args() + p = conf.pop('priority', None) + if p is None: + p = getattr(self.callable, 'priority', self._priority) + cherrypy.serving.request.hooks.attach(self._point, self.callable, + priority=p, **conf) + + +class HandlerTool(Tool): + + """Tool which is called 'before main', that may skip normal handlers. + + If the tool successfully handles the request (by setting response.body), + if should return True. This will cause CherryPy to skip any 'normal' page + handler. If the tool did not handle the request, it should return False + to tell CherryPy to continue on and call the normal page handler. If the + tool is declared AS a page handler (see the 'handler' method), returning + False will raise NotFound. + """ + + def __init__(self, callable, name=None): + Tool.__init__(self, 'before_handler', callable, name) + + def handler(self, *args, **kwargs): + """Use this tool as a CherryPy page handler. + + For example:: + + class Root: + nav = tools.staticdir.handler(section="/nav", dir="nav", + root=absDir) + """ + @expose + def handle_func(*a, **kw): + handled = self.callable(*args, **self._merged_args(kwargs)) + if not handled: + raise cherrypy.NotFound() + return cherrypy.serving.response.body + return handle_func + + def _wrapper(self, **kwargs): + if self.callable(**kwargs): + cherrypy.serving.request.handler = None + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + conf = self._merged_args() + p = conf.pop('priority', None) + if p is None: + p = getattr(self.callable, 'priority', self._priority) + cherrypy.serving.request.hooks.attach(self._point, self._wrapper, + priority=p, **conf) + + +class HandlerWrapperTool(Tool): + + """Tool which wraps request.handler in a provided wrapper function. + + The 'newhandler' arg must be a handler wrapper function that takes a + 'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all + page handler + functions, it must return an iterable for use as cherrypy.response.body. + + For example, to allow your 'inner' page handlers to return dicts + which then get interpolated into a template:: + + def interpolator(next_handler, *args, **kwargs): + filename = cherrypy.request.config.get('template') + cherrypy.response.template = env.get_template(filename) + response_dict = next_handler(*args, **kwargs) + return cherrypy.response.template.render(**response_dict) + cherrypy.tools.jinja = HandlerWrapperTool(interpolator) + """ + + def __init__(self, newhandler, point='before_handler', name=None, + priority=50): + self.newhandler = newhandler + self._point = point + self._name = name + self._priority = priority + + def callable(self, *args, **kwargs): + innerfunc = cherrypy.serving.request.handler + + def wrap(*args, **kwargs): + return self.newhandler(innerfunc, *args, **kwargs) + cherrypy.serving.request.handler = wrap + + +class ErrorTool(Tool): + + """Tool which is used to replace the default request.error_response.""" + + def __init__(self, callable, name=None): + Tool.__init__(self, None, callable, name) + + def _wrapper(self): + self.callable(**self._merged_args()) + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + cherrypy.serving.request.error_response = self._wrapper + + +# Builtin tools # + + +class SessionTool(Tool): + + """Session Tool for CherryPy. + + sessions.locking + When 'implicit' (the default), the session will be locked for you, + just before running the page handler. + + When 'early', the session will be locked before reading the request + body. This is off by default for safety reasons; for example, + a large upload would block the session, denying an AJAX + progress meter + (`issue `_). + + When 'explicit' (or any other value), you need to call + cherrypy.session.acquire_lock() yourself before using + session data. + """ + + def __init__(self): + # _sessions.init must be bound after headers are read + Tool.__init__(self, 'before_request_body', _sessions.init) + + def _lock_session(self): + cherrypy.serving.session.acquire_lock() + + def _setup(self): + """Hook this tool into cherrypy.request. + + The standard CherryPy request object will automatically call this + method when the tool is "turned on" in config. + """ + hooks = cherrypy.serving.request.hooks + + conf = self._merged_args() + + p = conf.pop('priority', None) + if p is None: + p = getattr(self.callable, 'priority', self._priority) + + hooks.attach(self._point, self.callable, priority=p, **conf) + + locking = conf.pop('locking', 'implicit') + if locking == 'implicit': + hooks.attach('before_handler', self._lock_session) + elif locking == 'early': + # Lock before the request body (but after _sessions.init runs!) + hooks.attach('before_request_body', self._lock_session, + priority=60) + else: + # Don't lock + pass + + hooks.attach('before_finalize', _sessions.save) + hooks.attach('on_end_request', _sessions.close) + + def regenerate(self): + """Drop the current session and make a new one (with a new id).""" + sess = cherrypy.serving.session + sess.regenerate() + + # Grab cookie-relevant tool args + relevant = 'path', 'path_header', 'name', 'timeout', 'domain', 'secure' + conf = dict( + (k, v) + for k, v in self._merged_args().items() + if k in relevant + ) + _sessions.set_response_cookie(**conf) + + +class XMLRPCController(object): + + """A Controller (page handler collection) for XML-RPC. + + To use it, have your controllers subclass this base class (it will + turn on the tool for you). + + You can also supply the following optional config entries:: + + tools.xmlrpc.encoding: 'utf-8' + tools.xmlrpc.allow_none: 0 + + XML-RPC is a rather discontinuous layer over HTTP; dispatching to the + appropriate handler must first be performed according to the URL, and + then a second dispatch step must take place according to the RPC method + specified in the request body. It also allows a superfluous "/RPC2" + prefix in the URL, supplies its own handler args in the body, and + requires a 200 OK "Fault" response instead of 404 when the desired + method is not found. + + Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone. + This Controller acts as the dispatch target for the first half (based + on the URL); it then reads the RPC method from the request body and + does its own second dispatch step based on that method. It also reads + body params, and returns a Fault on error. + + The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2 + in your URL's, you can safely skip turning on the XMLRPCDispatcher. + Otherwise, you need to use declare it in config:: + + request.dispatch: cherrypy.dispatch.XMLRPCDispatcher() + """ + + # Note we're hard-coding this into the 'tools' namespace. We could do + # a huge amount of work to make it relocatable, but the only reason why + # would be if someone actually disabled the default_toolbox. Meh. + _cp_config = {'tools.xmlrpc.on': True} + + @expose + def default(self, *vpath, **params): + rpcparams, rpcmethod = _xmlrpc.process_body() + + subhandler = self + for attr in str(rpcmethod).split('.'): + subhandler = getattr(subhandler, attr, None) + + if subhandler and getattr(subhandler, 'exposed', False): + body = subhandler(*(vpath + rpcparams), **params) + + else: + # https://github.com/cherrypy/cherrypy/issues/533 + # if a method is not found, an xmlrpclib.Fault should be returned + # raising an exception here will do that; see + # cherrypy.lib.xmlrpcutil.on_error + raise Exception('method "%s" is not supported' % attr) + + conf = cherrypy.serving.request.toolmaps['tools'].get('xmlrpc', {}) + _xmlrpc.respond(body, + conf.get('encoding', 'utf-8'), + conf.get('allow_none', 0)) + return cherrypy.serving.response.body + + +class SessionAuthTool(HandlerTool): + pass + + +class CachingTool(Tool): + + """Caching Tool for CherryPy.""" + + def _wrapper(self, **kwargs): + request = cherrypy.serving.request + if _caching.get(**kwargs): + request.handler = None + else: + if request.cacheable: + # Note the devious technique here of adding hooks on the fly + request.hooks.attach('before_finalize', _caching.tee_output, + priority=100) + _wrapper.priority = 90 + + def _setup(self): + """Hook caching into cherrypy.request.""" + conf = self._merged_args() + + p = conf.pop('priority', None) + cherrypy.serving.request.hooks.attach('before_handler', self._wrapper, + priority=p, **conf) + + +class Toolbox(object): + + """A collection of Tools. + + This object also functions as a config namespace handler for itself. + Custom toolboxes should be added to each Application's toolboxes dict. + """ + + def __init__(self, namespace): + self.namespace = namespace + + def __setattr__(self, name, value): + # If the Tool._name is None, supply it from the attribute name. + if isinstance(value, Tool): + if value._name is None: + value._name = name + value.namespace = self.namespace + object.__setattr__(self, name, value) + + def __enter__(self): + """Populate request.toolmaps from tools specified in config.""" + cherrypy.serving.request.toolmaps[self.namespace] = map = {} + + def populate(k, v): + toolname, arg = k.split('.', 1) + bucket = map.setdefault(toolname, {}) + bucket[arg] = v + return populate + + def __exit__(self, exc_type, exc_val, exc_tb): + """Run tool._setup() for each tool in our toolmap.""" + map = cherrypy.serving.request.toolmaps.get(self.namespace) + if map: + for name, settings in map.items(): + if settings.get('on', False): + tool = getattr(self, name) + tool._setup() + + def register(self, point, **kwargs): + """ + Return a decorator which registers the function + at the given hook point. + """ + def decorator(func): + attr_name = kwargs.get('name', func.__name__) + tool = Tool(point, func, **kwargs) + setattr(self, attr_name, tool) + return func + return decorator + + +default_toolbox = _d = Toolbox('tools') +_d.session_auth = SessionAuthTool(cptools.session_auth) +_d.allow = Tool('on_start_resource', cptools.allow) +_d.proxy = Tool('before_request_body', cptools.proxy, priority=30) +_d.response_headers = Tool('on_start_resource', cptools.response_headers) +_d.log_tracebacks = Tool('before_error_response', cptools.log_traceback) +_d.log_headers = Tool('before_error_response', cptools.log_request_headers) +_d.log_hooks = Tool('on_end_request', cptools.log_hooks, priority=100) +_d.err_redirect = ErrorTool(cptools.redirect) +_d.etags = Tool('before_finalize', cptools.validate_etags, priority=75) +_d.decode = Tool('before_request_body', encoding.decode) +# the order of encoding, gzip, caching is important +_d.encode = Tool('before_handler', encoding.ResponseEncoder, priority=70) +_d.gzip = Tool('before_finalize', encoding.gzip, priority=80) +_d.staticdir = HandlerTool(static.staticdir) +_d.staticfile = HandlerTool(static.staticfile) +_d.sessions = SessionTool() +_d.xmlrpc = ErrorTool(_xmlrpc.on_error) +_d.caching = CachingTool('before_handler', _caching.get, 'caching') +_d.expires = Tool('before_finalize', _caching.expires) +_d.ignore_headers = Tool('before_request_body', cptools.ignore_headers) +_d.referer = Tool('before_request_body', cptools.referer) +_d.trailing_slash = Tool('before_handler', cptools.trailing_slash, priority=60) +_d.flatten = Tool('before_finalize', cptools.flatten) +_d.accept = Tool('on_start_resource', cptools.accept) +_d.redirect = Tool('on_start_resource', cptools.redirect) +_d.autovary = Tool('on_start_resource', cptools.autovary, priority=0) +_d.json_in = Tool('before_request_body', jsontools.json_in, priority=30) +_d.json_out = Tool('before_handler', jsontools.json_out, priority=30) +_d.auth_basic = Tool('before_handler', auth_basic.basic_auth, priority=1) +_d.auth_digest = Tool('before_handler', auth_digest.digest_auth, priority=1) +_d.params = Tool('before_handler', cptools.convert_params, priority=15) + +del _d, cptools, encoding, static diff --git a/resources/lib/cherrypy/_cptree.py b/resources/lib/cherrypy/_cptree.py new file mode 100644 index 0000000..ceb5437 --- /dev/null +++ b/resources/lib/cherrypy/_cptree.py @@ -0,0 +1,313 @@ +"""CherryPy Application and Tree objects.""" + +import os + +import six + +import cherrypy +from cherrypy._cpcompat import ntou +from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools +from cherrypy.lib import httputil, reprconf + + +class Application(object): + """A CherryPy Application. + + Servers and gateways should not instantiate Request objects directly. + Instead, they should ask an Application object for a request object. + + An instance of this class may also be used as a WSGI callable + (WSGI application object) for itself. + """ + + root = None + """The top-most container of page handlers for this app. Handlers should + be arranged in a hierarchy of attributes, matching the expected URI + hierarchy; the default dispatcher then searches this hierarchy for a + matching handler. When using a dispatcher other than the default, + this value may be None.""" + + config = {} + """A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict + of {key: value} pairs.""" + + namespaces = reprconf.NamespaceSet() + toolboxes = {'tools': cherrypy.tools} + + log = None + """A LogManager instance. See _cplogging.""" + + wsgiapp = None + """A CPWSGIApp instance. See _cpwsgi.""" + + request_class = _cprequest.Request + response_class = _cprequest.Response + + relative_urls = False + + def __init__(self, root, script_name='', config=None): + """Initialize Application with given root.""" + self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root) + self.root = root + self.script_name = script_name + self.wsgiapp = _cpwsgi.CPWSGIApp(self) + + self.namespaces = self.namespaces.copy() + self.namespaces['log'] = lambda k, v: setattr(self.log, k, v) + self.namespaces['wsgi'] = self.wsgiapp.namespace_handler + + self.config = self.__class__.config.copy() + if config: + self.merge(config) + + def __repr__(self): + """Generate a representation of the Application instance.""" + return '%s.%s(%r, %r)' % (self.__module__, self.__class__.__name__, + self.root, self.script_name) + + script_name_doc = """The URI "mount point" for this app. A mount point + is that portion of the URI which is constant for all URIs that are + serviced by this application; it does not include scheme, host, or proxy + ("virtual host") portions of the URI. + + For example, if script_name is "/my/cool/app", then the URL + "http://www.example.com/my/cool/app/page1" might be handled by a + "page1" method on the root object. + + The value of script_name MUST NOT end in a slash. If the script_name + refers to the root of the URI, it MUST be an empty string (not "/"). + + If script_name is explicitly set to None, then the script_name will be + provided for each call from request.wsgi_environ['SCRIPT_NAME']. + """ + + @property + def script_name(self): # noqa: D401; irrelevant for properties + """The URI "mount point" for this app. + + A mount point is that portion of the URI which is constant for all URIs + that are serviced by this application; it does not include scheme, + host, or proxy ("virtual host") portions of the URI. + + For example, if script_name is "/my/cool/app", then the URL + "http://www.example.com/my/cool/app/page1" might be handled by a + "page1" method on the root object. + + The value of script_name MUST NOT end in a slash. If the script_name + refers to the root of the URI, it MUST be an empty string (not "/"). + + If script_name is explicitly set to None, then the script_name will be + provided for each call from request.wsgi_environ['SCRIPT_NAME']. + """ + if self._script_name is not None: + return self._script_name + + # A `_script_name` with a value of None signals that the script name + # should be pulled from WSGI environ. + return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip('/') + + @script_name.setter + def script_name(self, value): + if value: + value = value.rstrip('/') + self._script_name = value + + def merge(self, config): + """Merge the given config into self.config.""" + _cpconfig.merge(self.config, config) + + # Handle namespaces specified in config. + self.namespaces(self.config.get('/', {})) + + def find_config(self, path, key, default=None): + """Return the most-specific value for key along path, or default.""" + trail = path or '/' + while trail: + nodeconf = self.config.get(trail, {}) + + if key in nodeconf: + return nodeconf[key] + + lastslash = trail.rfind('/') + if lastslash == -1: + break + elif lastslash == 0 and trail != '/': + trail = '/' + else: + trail = trail[:lastslash] + + return default + + def get_serving(self, local, remote, scheme, sproto): + """Create and return a Request and Response object.""" + req = self.request_class(local, remote, scheme, sproto) + req.app = self + + for name, toolbox in self.toolboxes.items(): + req.namespaces[name] = toolbox + + resp = self.response_class() + cherrypy.serving.load(req, resp) + cherrypy.engine.publish('acquire_thread') + cherrypy.engine.publish('before_request') + + return req, resp + + def release_serving(self): + """Release the current serving (request and response).""" + req = cherrypy.serving.request + + cherrypy.engine.publish('after_request') + + try: + req.close() + except Exception: + cherrypy.log(traceback=True, severity=40) + + cherrypy.serving.clear() + + def __call__(self, environ, start_response): + """Call a WSGI-callable.""" + return self.wsgiapp(environ, start_response) + + +class Tree(object): + """A registry of CherryPy applications, mounted at diverse points. + + An instance of this class may also be used as a WSGI callable + (WSGI application object), in which case it dispatches to all + mounted apps. + """ + + apps = {} + """ + A dict of the form {script name: application}, where "script name" + is a string declaring the URI mount point (no trailing slash), and + "application" is an instance of cherrypy.Application (or an arbitrary + WSGI callable if you happen to be using a WSGI server).""" + + def __init__(self): + """Initialize registry Tree.""" + self.apps = {} + + def mount(self, root, script_name='', config=None): + """Mount a new app from a root object, script_name, and config. + + root + An instance of a "controller class" (a collection of page + handler methods) which represents the root of the application. + This may also be an Application instance, or None if using + a dispatcher other than the default. + + script_name + A string containing the "mount point" of the application. + This should start with a slash, and be the path portion of the + URL at which to mount the given root. For example, if root.index() + will handle requests to "http://www.example.com:8080/dept/app1/", + then the script_name argument would be "/dept/app1". + + It MUST NOT end in a slash. If the script_name refers to the + root of the URI, it MUST be an empty string (not "/"). + + config + A file or dict containing application config. + """ + if script_name is None: + raise TypeError( + "The 'script_name' argument may not be None. Application " + 'objects may, however, possess a script_name of None (in ' + 'order to inpect the WSGI environ for SCRIPT_NAME upon each ' + 'request). You cannot mount such Applications on this Tree; ' + 'you must pass them to a WSGI server interface directly.') + + # Next line both 1) strips trailing slash and 2) maps "/" -> "". + script_name = script_name.rstrip('/') + + if isinstance(root, Application): + app = root + if script_name != '' and script_name != app.script_name: + raise ValueError( + 'Cannot specify a different script name and pass an ' + 'Application instance to cherrypy.mount') + script_name = app.script_name + else: + app = Application(root, script_name) + + # If mounted at "", add favicon.ico + needs_favicon = ( + script_name == '' + and root is not None + and not hasattr(root, 'favicon_ico') + ) + if needs_favicon: + favicon = os.path.join( + os.getcwd(), + os.path.dirname(__file__), + 'favicon.ico', + ) + root.favicon_ico = tools.staticfile.handler(favicon) + + if config: + app.merge(config) + + self.apps[script_name] = app + + return app + + def graft(self, wsgi_callable, script_name=''): + """Mount a wsgi callable at the given script_name.""" + # Next line both 1) strips trailing slash and 2) maps "/" -> "". + script_name = script_name.rstrip('/') + self.apps[script_name] = wsgi_callable + + def script_name(self, path=None): + """Return the script_name of the app at the given path, or None. + + If path is None, cherrypy.request is used. + """ + if path is None: + try: + request = cherrypy.serving.request + path = httputil.urljoin(request.script_name, + request.path_info) + except AttributeError: + return None + + while True: + if path in self.apps: + return path + + if path == '': + return None + + # Move one node up the tree and try again. + path = path[:path.rfind('/')] + + def __call__(self, environ, start_response): + """Pre-initialize WSGI env and call WSGI-callable.""" + # If you're calling this, then you're probably setting SCRIPT_NAME + # to '' (some WSGI servers always set SCRIPT_NAME to ''). + # Try to look up the app using the full path. + env1x = environ + if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0): + env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ) + path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''), + env1x.get('PATH_INFO', '')) + sn = self.script_name(path or '/') + if sn is None: + start_response('404 Not Found', []) + return [] + + app = self.apps[sn] + + # Correct the SCRIPT_NAME and PATH_INFO environ entries. + environ = environ.copy() + if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0): + # Python 2/WSGI u.0: all strings MUST be of type unicode + enc = environ[ntou('wsgi.url_encoding')] + environ[ntou('SCRIPT_NAME')] = sn.decode(enc) + environ[ntou('PATH_INFO')] = path[len(sn.rstrip('/')):].decode(enc) + else: + environ['SCRIPT_NAME'] = sn + environ['PATH_INFO'] = path[len(sn.rstrip('/')):] + return app(environ, start_response) diff --git a/resources/lib/cherrypy/_cpwsgi.py b/resources/lib/cherrypy/_cpwsgi.py new file mode 100644 index 0000000..0b4942f --- /dev/null +++ b/resources/lib/cherrypy/_cpwsgi.py @@ -0,0 +1,467 @@ +"""WSGI interface (see PEP 333 and 3333). + +Note that WSGI environ keys and values are 'native strings'; that is, +whatever the type of "" is. For Python 2, that's a byte string; for Python 3, +it's a unicode string. But PEP 3333 says: "even if Python's str type is +actually Unicode "under the hood", the content of native strings must +still be translatable to bytes via the Latin-1 encoding!" +""" + +import sys as _sys +import io + +import six + +import cherrypy as _cherrypy +from cherrypy._cpcompat import ntou +from cherrypy import _cperror +from cherrypy.lib import httputil +from cherrypy.lib import is_closable_iterator + + +def downgrade_wsgi_ux_to_1x(environ): + """Return a new environ dict for WSGI 1.x from the given WSGI u.x environ. + """ + env1x = {} + + url_encoding = environ[ntou('wsgi.url_encoding')] + for k, v in list(environ.items()): + if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]: + v = v.encode(url_encoding) + elif isinstance(v, six.text_type): + v = v.encode('ISO-8859-1') + env1x[k.encode('ISO-8859-1')] = v + + return env1x + + +class VirtualHost(object): + + """Select a different WSGI application based on the Host header. + + This can be useful when running multiple sites within one CP server. + It allows several domains to point to different applications. For example:: + + root = Root() + RootApp = cherrypy.Application(root) + Domain2App = cherrypy.Application(root) + SecureApp = cherrypy.Application(Secure()) + + vhost = cherrypy._cpwsgi.VirtualHost( + RootApp, + domains={ + 'www.domain2.example': Domain2App, + 'www.domain2.example:443': SecureApp, + }, + ) + + cherrypy.tree.graft(vhost) + """ + default = None + """Required. The default WSGI application.""" + + use_x_forwarded_host = True + """If True (the default), any "X-Forwarded-Host" + request header will be used instead of the "Host" header. This + is commonly added by HTTP servers (such as Apache) when proxying.""" + + domains = {} + """A dict of {host header value: application} pairs. + The incoming "Host" request header is looked up in this dict, + and, if a match is found, the corresponding WSGI application + will be called instead of the default. Note that you often need + separate entries for "example.com" and "www.example.com". + In addition, "Host" headers may contain the port number. + """ + + def __init__(self, default, domains=None, use_x_forwarded_host=True): + self.default = default + self.domains = domains or {} + self.use_x_forwarded_host = use_x_forwarded_host + + def __call__(self, environ, start_response): + domain = environ.get('HTTP_HOST', '') + if self.use_x_forwarded_host: + domain = environ.get('HTTP_X_FORWARDED_HOST', domain) + + nextapp = self.domains.get(domain) + if nextapp is None: + nextapp = self.default + return nextapp(environ, start_response) + + +class InternalRedirector(object): + + """WSGI middleware that handles raised cherrypy.InternalRedirect.""" + + def __init__(self, nextapp, recursive=False): + self.nextapp = nextapp + self.recursive = recursive + + def __call__(self, environ, start_response): + redirections = [] + while True: + environ = environ.copy() + try: + return self.nextapp(environ, start_response) + except _cherrypy.InternalRedirect: + ir = _sys.exc_info()[1] + sn = environ.get('SCRIPT_NAME', '') + path = environ.get('PATH_INFO', '') + qs = environ.get('QUERY_STRING', '') + + # Add the *previous* path_info + qs to redirections. + old_uri = sn + path + if qs: + old_uri += '?' + qs + redirections.append(old_uri) + + if not self.recursive: + # Check to see if the new URI has been redirected to + # already + new_uri = sn + ir.path + if ir.query_string: + new_uri += '?' + ir.query_string + if new_uri in redirections: + ir.request.close() + tmpl = ( + 'InternalRedirector visited the same URL twice: %r' + ) + raise RuntimeError(tmpl % new_uri) + + # Munge the environment and try again. + environ['REQUEST_METHOD'] = 'GET' + environ['PATH_INFO'] = ir.path + environ['QUERY_STRING'] = ir.query_string + environ['wsgi.input'] = io.BytesIO() + environ['CONTENT_LENGTH'] = '0' + environ['cherrypy.previous_request'] = ir.request + + +class ExceptionTrapper(object): + + """WSGI middleware that traps exceptions.""" + + def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)): + self.nextapp = nextapp + self.throws = throws + + def __call__(self, environ, start_response): + return _TrappedResponse( + self.nextapp, + environ, + start_response, + self.throws + ) + + +class _TrappedResponse(object): + + response = iter([]) + + def __init__(self, nextapp, environ, start_response, throws): + self.nextapp = nextapp + self.environ = environ + self.start_response = start_response + self.throws = throws + self.started_response = False + self.response = self.trap( + self.nextapp, self.environ, self.start_response, + ) + self.iter_response = iter(self.response) + + def __iter__(self): + self.started_response = True + return self + + def __next__(self): + return self.trap(next, self.iter_response) + + # todo: https://pythonhosted.org/six/#six.Iterator + if six.PY2: + next = __next__ + + def close(self): + if hasattr(self.response, 'close'): + self.response.close() + + def trap(self, func, *args, **kwargs): + try: + return func(*args, **kwargs) + except self.throws: + raise + except StopIteration: + raise + except Exception: + tb = _cperror.format_exc() + _cherrypy.log(tb, severity=40) + if not _cherrypy.request.show_tracebacks: + tb = '' + s, h, b = _cperror.bare_error(tb) + if six.PY3: + # What fun. + s = s.decode('ISO-8859-1') + h = [ + (k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) + for k, v in h + ] + if self.started_response: + # Empty our iterable (so future calls raise StopIteration) + self.iter_response = iter([]) + else: + self.iter_response = iter(b) + + try: + self.start_response(s, h, _sys.exc_info()) + except Exception: + # "The application must not trap any exceptions raised by + # start_response, if it called start_response with exc_info. + # Instead, it should allow such exceptions to propagate + # back to the server or gateway." + # But we still log and call close() to clean up ourselves. + _cherrypy.log(traceback=True, severity=40) + raise + + if self.started_response: + return b''.join(b) + else: + return b + + +# WSGI-to-CP Adapter # + + +class AppResponse(object): + + """WSGI response iterable for CherryPy applications.""" + + def __init__(self, environ, start_response, cpapp): + self.cpapp = cpapp + try: + if six.PY2: + if environ.get(ntou('wsgi.version')) == (ntou('u'), 0): + environ = downgrade_wsgi_ux_to_1x(environ) + self.environ = environ + self.run() + + r = _cherrypy.serving.response + + outstatus = r.output_status + if not isinstance(outstatus, bytes): + raise TypeError('response.output_status is not a byte string.') + + outheaders = [] + for k, v in r.header_list: + if not isinstance(k, bytes): + tmpl = 'response.header_list key %r is not a byte string.' + raise TypeError(tmpl % k) + if not isinstance(v, bytes): + tmpl = ( + 'response.header_list value %r is not a byte string.' + ) + raise TypeError(tmpl % v) + outheaders.append((k, v)) + + if six.PY3: + # According to PEP 3333, when using Python 3, the response + # status and headers must be bytes masquerading as unicode; + # that is, they must be of type "str" but are restricted to + # code points in the "latin-1" set. + outstatus = outstatus.decode('ISO-8859-1') + outheaders = [ + (k.decode('ISO-8859-1'), v.decode('ISO-8859-1')) + for k, v in outheaders + ] + + self.iter_response = iter(r.body) + self.write = start_response(outstatus, outheaders) + except BaseException: + self.close() + raise + + def __iter__(self): + return self + + def __next__(self): + return next(self.iter_response) + + # todo: https://pythonhosted.org/six/#six.Iterator + if six.PY2: + next = __next__ + + def close(self): + """Close and de-reference the current request and response. (Core)""" + streaming = _cherrypy.serving.response.stream + self.cpapp.release_serving() + + # We avoid the expense of examining the iterator to see if it's + # closable unless we are streaming the response, as that's the + # only situation where we are going to have an iterator which + # may not have been exhausted yet. + if streaming and is_closable_iterator(self.iter_response): + iter_close = self.iter_response.close + try: + iter_close() + except Exception: + _cherrypy.log(traceback=True, severity=40) + + def run(self): + """Create a Request object using environ.""" + env = self.environ.get + + local = httputil.Host( + '', + int(env('SERVER_PORT', 80) or -1), + env('SERVER_NAME', ''), + ) + remote = httputil.Host( + env('REMOTE_ADDR', ''), + int(env('REMOTE_PORT', -1) or -1), + env('REMOTE_HOST', ''), + ) + scheme = env('wsgi.url_scheme') + sproto = env('ACTUAL_SERVER_PROTOCOL', 'HTTP/1.1') + request, resp = self.cpapp.get_serving(local, remote, scheme, sproto) + + # LOGON_USER is served by IIS, and is the name of the + # user after having been mapped to a local account. + # Both IIS and Apache set REMOTE_USER, when possible. + request.login = env('LOGON_USER') or env('REMOTE_USER') or None + request.multithread = self.environ['wsgi.multithread'] + request.multiprocess = self.environ['wsgi.multiprocess'] + request.wsgi_environ = self.environ + request.prev = env('cherrypy.previous_request', None) + + meth = self.environ['REQUEST_METHOD'] + + path = httputil.urljoin( + self.environ.get('SCRIPT_NAME', ''), + self.environ.get('PATH_INFO', ''), + ) + qs = self.environ.get('QUERY_STRING', '') + + path, qs = self.recode_path_qs(path, qs) or (path, qs) + + rproto = self.environ.get('SERVER_PROTOCOL') + headers = self.translate_headers(self.environ) + rfile = self.environ['wsgi.input'] + request.run(meth, path, qs, rproto, headers, rfile) + + headerNames = { + 'HTTP_CGI_AUTHORIZATION': 'Authorization', + 'CONTENT_LENGTH': 'Content-Length', + 'CONTENT_TYPE': 'Content-Type', + 'REMOTE_HOST': 'Remote-Host', + 'REMOTE_ADDR': 'Remote-Addr', + } + + def recode_path_qs(self, path, qs): + if not six.PY3: + return + + # This isn't perfect; if the given PATH_INFO is in the + # wrong encoding, it may fail to match the appropriate config + # section URI. But meh. + old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1') + new_enc = self.cpapp.find_config( + self.environ.get('PATH_INFO', ''), + 'request.uri_encoding', 'utf-8', + ) + if new_enc.lower() == old_enc.lower(): + return + + # Even though the path and qs are unicode, the WSGI server + # is required by PEP 3333 to coerce them to ISO-8859-1 + # masquerading as unicode. So we have to encode back to + # bytes and then decode again using the "correct" encoding. + try: + return ( + path.encode(old_enc).decode(new_enc), + qs.encode(old_enc).decode(new_enc), + ) + except (UnicodeEncodeError, UnicodeDecodeError): + # Just pass them through without transcoding and hope. + pass + + def translate_headers(self, environ): + """Translate CGI-environ header names to HTTP header names.""" + for cgiName in environ: + # We assume all incoming header keys are uppercase already. + if cgiName in self.headerNames: + yield self.headerNames[cgiName], environ[cgiName] + elif cgiName[:5] == 'HTTP_': + # Hackish attempt at recovering original header names. + translatedHeader = cgiName[5:].replace('_', '-') + yield translatedHeader, environ[cgiName] + + +class CPWSGIApp(object): + + """A WSGI application object for a CherryPy Application.""" + + pipeline = [ + ('ExceptionTrapper', ExceptionTrapper), + ('InternalRedirector', InternalRedirector), + ] + """A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a + constructor that takes an initial, positional 'nextapp' argument, + plus optional keyword arguments, and returns a WSGI application + (that takes environ and start_response arguments). The 'name' can + be any you choose, and will correspond to keys in self.config.""" + + head = None + """Rather than nest all apps in the pipeline on each call, it's only + done the first time, and the result is memoized into self.head. Set + this to None again if you change self.pipeline after calling self.""" + + config = {} + """A dict whose keys match names listed in the pipeline. Each + value is a further dict which will be passed to the corresponding + named WSGI callable (from the pipeline) as keyword arguments.""" + + response_class = AppResponse + """The class to instantiate and return as the next app in the WSGI chain. + """ + + def __init__(self, cpapp, pipeline=None): + self.cpapp = cpapp + self.pipeline = self.pipeline[:] + if pipeline: + self.pipeline.extend(pipeline) + self.config = self.config.copy() + + def tail(self, environ, start_response): + """WSGI application callable for the actual CherryPy application. + + You probably shouldn't call this; call self.__call__ instead, + so that any WSGI middleware in self.pipeline can run first. + """ + return self.response_class(environ, start_response, self.cpapp) + + def __call__(self, environ, start_response): + head = self.head + if head is None: + # Create and nest the WSGI apps in our pipeline (in reverse order). + # Then memoize the result in self.head. + head = self.tail + for name, callable in self.pipeline[::-1]: + conf = self.config.get(name, {}) + head = callable(head, **conf) + self.head = head + return head(environ, start_response) + + def namespace_handler(self, k, v): + """Config handler for the 'wsgi' namespace.""" + if k == 'pipeline': + # Note this allows multiple 'wsgi.pipeline' config entries + # (but each entry will be processed in a 'random' order). + # It should also allow developers to set default middleware + # in code (passed to self.__init__) that deployers can add to + # (but not remove) via config. + self.pipeline.extend(v) + elif k == 'response_class': + self.response_class = v + else: + name, arg = k.split('.', 1) + bucket = self.config.setdefault(name, {}) + bucket[arg] = v diff --git a/resources/lib/cherrypy/_cpwsgi_server.py b/resources/lib/cherrypy/_cpwsgi_server.py new file mode 100644 index 0000000..11dd846 --- /dev/null +++ b/resources/lib/cherrypy/_cpwsgi_server.py @@ -0,0 +1,110 @@ +""" +WSGI server interface (see PEP 333). + +This adds some CP-specific bits to the framework-agnostic cheroot package. +""" +import sys + +import cheroot.wsgi +import cheroot.server + +import cherrypy + + +class CPWSGIHTTPRequest(cheroot.server.HTTPRequest): + """Wrapper for cheroot.server.HTTPRequest. + + This is a layer, which preserves URI parsing mode like it which was + before Cheroot v5.8.0. + """ + + def __init__(self, server, conn): + """Initialize HTTP request container instance. + + Args: + server (cheroot.server.HTTPServer): + web server object receiving this request + conn (cheroot.server.HTTPConnection): + HTTP connection object for this request + """ + super(CPWSGIHTTPRequest, self).__init__( + server, conn, proxy_mode=True + ) + + +class CPWSGIServer(cheroot.wsgi.Server): + """Wrapper for cheroot.wsgi.Server. + + cheroot has been designed to not reference CherryPy in any way, + so that it can be used in other frameworks and applications. Therefore, + we wrap it here, so we can set our own mount points from cherrypy.tree + and apply some attributes from config -> cherrypy.server -> wsgi.Server. + """ + + fmt = 'CherryPy/{cherrypy.__version__} {cheroot.wsgi.Server.version}' + version = fmt.format(**globals()) + + def __init__(self, server_adapter=cherrypy.server): + """Initialize CPWSGIServer instance. + + Args: + server_adapter (cherrypy._cpserver.Server): ... + """ + self.server_adapter = server_adapter + self.max_request_header_size = ( + self.server_adapter.max_request_header_size or 0 + ) + self.max_request_body_size = ( + self.server_adapter.max_request_body_size or 0 + ) + + server_name = (self.server_adapter.socket_host or + self.server_adapter.socket_file or + None) + + self.wsgi_version = self.server_adapter.wsgi_version + + super(CPWSGIServer, self).__init__( + server_adapter.bind_addr, cherrypy.tree, + self.server_adapter.thread_pool, + server_name, + max=self.server_adapter.thread_pool_max, + request_queue_size=self.server_adapter.socket_queue_size, + timeout=self.server_adapter.socket_timeout, + shutdown_timeout=self.server_adapter.shutdown_timeout, + accepted_queue_size=self.server_adapter.accepted_queue_size, + accepted_queue_timeout=self.server_adapter.accepted_queue_timeout, + peercreds_enabled=self.server_adapter.peercreds, + peercreds_resolve_enabled=self.server_adapter.peercreds_resolve, + ) + self.ConnectionClass.RequestHandlerClass = CPWSGIHTTPRequest + + self.protocol = self.server_adapter.protocol_version + self.nodelay = self.server_adapter.nodelay + + if sys.version_info >= (3, 0): + ssl_module = self.server_adapter.ssl_module or 'builtin' + else: + ssl_module = self.server_adapter.ssl_module or 'pyopenssl' + if self.server_adapter.ssl_context: + adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain, + self.server_adapter.ssl_ciphers) + self.ssl_adapter.context = self.server_adapter.ssl_context + elif self.server_adapter.ssl_certificate: + adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module) + self.ssl_adapter = adapter_class( + self.server_adapter.ssl_certificate, + self.server_adapter.ssl_private_key, + self.server_adapter.ssl_certificate_chain, + self.server_adapter.ssl_ciphers) + + self.stats['Enabled'] = getattr( + self.server_adapter, 'statistics', False) + + def error_log(self, msg='', level=20, traceback=False): + """Write given message to the error log.""" + cherrypy.engine.log(msg, level, traceback) diff --git a/resources/lib/cherrypy/_helper.py b/resources/lib/cherrypy/_helper.py new file mode 100644 index 0000000..314550c --- /dev/null +++ b/resources/lib/cherrypy/_helper.py @@ -0,0 +1,344 @@ +"""Helper functions for CP apps.""" + +import six +from six.moves import urllib + +from cherrypy._cpcompat import text_or_bytes + +import cherrypy + + +def expose(func=None, alias=None): + """Expose the function or class. + + Optionally provide an alias or set of aliases. + """ + def expose_(func): + func.exposed = True + if alias is not None: + if isinstance(alias, text_or_bytes): + parents[alias.replace('.', '_')] = func + else: + for a in alias: + parents[a.replace('.', '_')] = func + return func + + import sys + import types + decoratable_types = types.FunctionType, types.MethodType, type, + if six.PY2: + # Old-style classes are type types.ClassType. + decoratable_types += types.ClassType, + if isinstance(func, decoratable_types): + if alias is None: + # @expose + func.exposed = True + return func + else: + # func = expose(func, alias) + parents = sys._getframe(1).f_locals + return expose_(func) + elif func is None: + if alias is None: + # @expose() + parents = sys._getframe(1).f_locals + return expose_ + else: + # @expose(alias="alias") or + # @expose(alias=["alias1", "alias2"]) + parents = sys._getframe(1).f_locals + return expose_ + else: + # @expose("alias") or + # @expose(["alias1", "alias2"]) + parents = sys._getframe(1).f_locals + alias = func + return expose_ + + +def popargs(*args, **kwargs): + """Decorate _cp_dispatch. + + (cherrypy.dispatch.Dispatcher.dispatch_method_name) + + Optional keyword argument: handler=(Object or Function) + + Provides a _cp_dispatch function that pops off path segments into + cherrypy.request.params under the names specified. The dispatch + is then forwarded on to the next vpath element. + + Note that any existing (and exposed) member function of the class that + popargs is applied to will override that value of the argument. For + instance, if you have a method named "list" on the class decorated with + popargs, then accessing "/list" will call that function instead of popping + it off as the requested parameter. This restriction applies to all + _cp_dispatch functions. The only way around this restriction is to create + a "blank class" whose only function is to provide _cp_dispatch. + + If there are path elements after the arguments, or more arguments + are requested than are available in the vpath, then the 'handler' + keyword argument specifies the next object to handle the parameterized + request. If handler is not specified or is None, then self is used. + If handler is a function rather than an instance, then that function + will be called with the args specified and the return value from that + function used as the next object INSTEAD of adding the parameters to + cherrypy.request.args. + + This decorator may be used in one of two ways: + + As a class decorator: + @cherrypy.popargs('year', 'month', 'day') + class Blog: + def index(self, year=None, month=None, day=None): + #Process the parameters here; any url like + #/, /2009, /2009/12, or /2009/12/31 + #will fill in the appropriate parameters. + + def create(self): + #This link will still be available at /create. Defined functions + #take precedence over arguments. + + Or as a member of a class: + class Blog: + _cp_dispatch = cherrypy.popargs('year', 'month', 'day') + #... + + The handler argument may be used to mix arguments with built in functions. + For instance, the following setup allows different activities at the + day, month, and year level: + + class DayHandler: + def index(self, year, month, day): + #Do something with this day; probably list entries + + def delete(self, year, month, day): + #Delete all entries for this day + + @cherrypy.popargs('day', handler=DayHandler()) + class MonthHandler: + def index(self, year, month): + #Do something with this month; probably list entries + + def delete(self, year, month): + #Delete all entries for this month + + @cherrypy.popargs('month', handler=MonthHandler()) + class YearHandler: + def index(self, year): + #Do something with this year + + #... + + @cherrypy.popargs('year', handler=YearHandler()) + class Root: + def index(self): + #... + + """ + # Since keyword arg comes after *args, we have to process it ourselves + # for lower versions of python. + + handler = None + handler_call = False + for k, v in kwargs.items(): + if k == 'handler': + handler = v + else: + tm = "cherrypy.popargs() got an unexpected keyword argument '{0}'" + raise TypeError(tm.format(k)) + + import inspect + + if handler is not None \ + and (hasattr(handler, '__call__') or inspect.isclass(handler)): + handler_call = True + + def decorated(cls_or_self=None, vpath=None): + if inspect.isclass(cls_or_self): + # cherrypy.popargs is a class decorator + cls = cls_or_self + name = cherrypy.dispatch.Dispatcher.dispatch_method_name + setattr(cls, name, decorated) + return cls + + # We're in the actual function + self = cls_or_self + parms = {} + for arg in args: + if not vpath: + break + parms[arg] = vpath.pop(0) + + if handler is not None: + if handler_call: + return handler(**parms) + else: + cherrypy.request.params.update(parms) + return handler + + cherrypy.request.params.update(parms) + + # If we are the ultimate handler, then to prevent our _cp_dispatch + # from being called again, we will resolve remaining elements through + # getattr() directly. + if vpath: + return getattr(self, vpath.pop(0), None) + else: + return self + + return decorated + + +def url(path='', qs='', script_name=None, base=None, relative=None): + """Create an absolute URL for the given path. + + If 'path' starts with a slash ('/'), this will return + (base + script_name + path + qs). + If it does not start with a slash, this returns + (base + script_name [+ request.path_info] + path + qs). + + If script_name is None, cherrypy.request will be used + to find a script_name, if available. + + If base is None, cherrypy.request.base will be used (if available). + Note that you can use cherrypy.tools.proxy to change this. + + Finally, note that this function can be used to obtain an absolute URL + for the current request path (minus the querystring) by passing no args. + If you call url(qs=cherrypy.request.query_string), you should get the + original browser URL (assuming no internal redirections). + + If relative is None or not provided, request.app.relative_urls will + be used (if available, else False). If False, the output will be an + absolute URL (including the scheme, host, vhost, and script_name). + If True, the output will instead be a URL that is relative to the + current request path, perhaps including '..' atoms. If relative is + the string 'server', the output will instead be a URL that is + relative to the server root; i.e., it will start with a slash. + """ + if isinstance(qs, (tuple, list, dict)): + qs = urllib.parse.urlencode(qs) + if qs: + qs = '?' + qs + + if cherrypy.request.app: + if not path.startswith('/'): + # Append/remove trailing slash from path_info as needed + # (this is to support mistyped URL's without redirecting; + # if you want to redirect, use tools.trailing_slash). + pi = cherrypy.request.path_info + if cherrypy.request.is_index is True: + if not pi.endswith('/'): + pi = pi + '/' + elif cherrypy.request.is_index is False: + if pi.endswith('/') and pi != '/': + pi = pi[:-1] + + if path == '': + path = pi + else: + path = urllib.parse.urljoin(pi, path) + + if script_name is None: + script_name = cherrypy.request.script_name + if base is None: + base = cherrypy.request.base + + newurl = base + script_name + normalize_path(path) + qs + else: + # No request.app (we're being called outside a request). + # We'll have to guess the base from server.* attributes. + # This will produce very different results from the above + # if you're using vhosts or tools.proxy. + if base is None: + base = cherrypy.server.base() + + path = (script_name or '') + path + newurl = base + normalize_path(path) + qs + + # At this point, we should have a fully-qualified absolute URL. + + if relative is None: + relative = getattr(cherrypy.request.app, 'relative_urls', False) + + # See http://www.ietf.org/rfc/rfc2396.txt + if relative == 'server': + # "A relative reference beginning with a single slash character is + # termed an absolute-path reference, as defined by ..." + # This is also sometimes called "server-relative". + newurl = '/' + '/'.join(newurl.split('/', 3)[3:]) + elif relative: + # "A relative reference that does not begin with a scheme name + # or a slash character is termed a relative-path reference." + old = url(relative=False).split('/')[:-1] + new = newurl.split('/') + while old and new: + a, b = old[0], new[0] + if a != b: + break + old.pop(0) + new.pop(0) + new = (['..'] * len(old)) + new + newurl = '/'.join(new) + + return newurl + + +def normalize_path(path): + """Resolve given path from relative into absolute form.""" + if './' not in path: + return path + + # Normalize the URL by removing ./ and ../ + atoms = [] + for atom in path.split('/'): + if atom == '.': + pass + elif atom == '..': + # Don't pop from empty list + # (i.e. ignore redundant '..') + if atoms: + atoms.pop() + elif atom: + atoms.append(atom) + + newpath = '/'.join(atoms) + # Preserve leading '/' + if path.startswith('/'): + newpath = '/' + newpath + + return newpath + + +#### +# Inlined from jaraco.classes 1.4.3 +# Ref #1673 +class _ClassPropertyDescriptor(object): + """Descript for read-only class-based property. + + Turns a classmethod-decorated func into a read-only property of that class + type (means the value cannot be set). + """ + + def __init__(self, fget, fset=None): + """Initialize a class property descriptor. + + Instantiated by ``_helper.classproperty``. + """ + self.fget = fget + self.fset = fset + + def __get__(self, obj, klass=None): + """Return property value.""" + if klass is None: + klass = type(obj) + return self.fget.__get__(obj, klass)() + + +def classproperty(func): # noqa: D401; irrelevant for properties + """Decorator like classmethod to implement a static class property.""" + if not isinstance(func, (classmethod, staticmethod)): + func = classmethod(func) + + return _ClassPropertyDescriptor(func) +#### diff --git a/resources/lib/cherrypy/daemon.py b/resources/lib/cherrypy/daemon.py new file mode 100644 index 0000000..74488c0 --- /dev/null +++ b/resources/lib/cherrypy/daemon.py @@ -0,0 +1,107 @@ +"""The CherryPy daemon.""" + +import sys + +import cherrypy +from cherrypy.process import plugins, servers +from cherrypy import Application + + +def start(configfiles=None, daemonize=False, environment=None, + fastcgi=False, scgi=False, pidfile=None, imports=None, + cgi=False): + """Subscribe all engine plugins and start the engine.""" + sys.path = [''] + sys.path + for i in imports or []: + exec('import %s' % i) + + for c in configfiles or []: + cherrypy.config.update(c) + # If there's only one app mounted, merge config into it. + if len(cherrypy.tree.apps) == 1: + for app in cherrypy.tree.apps.values(): + if isinstance(app, Application): + app.merge(c) + + engine = cherrypy.engine + + if environment is not None: + cherrypy.config.update({'environment': environment}) + + # Only daemonize if asked to. + if daemonize: + # Don't print anything to stdout/sterr. + cherrypy.config.update({'log.screen': False}) + plugins.Daemonizer(engine).subscribe() + + if pidfile: + plugins.PIDFile(engine, pidfile).subscribe() + + if hasattr(engine, 'signal_handler'): + engine.signal_handler.subscribe() + if hasattr(engine, 'console_control_handler'): + engine.console_control_handler.subscribe() + + if (fastcgi and (scgi or cgi)) or (scgi and cgi): + cherrypy.log.error('You may only specify one of the cgi, fastcgi, and ' + 'scgi options.', 'ENGINE') + sys.exit(1) + elif fastcgi or scgi or cgi: + # Turn off autoreload when using *cgi. + cherrypy.config.update({'engine.autoreload.on': False}) + # Turn off the default HTTP server (which is subscribed by default). + cherrypy.server.unsubscribe() + + addr = cherrypy.server.bind_addr + cls = ( + servers.FlupFCGIServer if fastcgi else + servers.FlupSCGIServer if scgi else + servers.FlupCGIServer + ) + f = cls(application=cherrypy.tree, bindAddress=addr) + s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr) + s.subscribe() + + # Always start the engine; this will start all other services + try: + engine.start() + except Exception: + # Assume the error has been logged already via bus.log. + sys.exit(1) + else: + engine.block() + + +def run(): + """Run cherryd CLI.""" + from optparse import OptionParser + + p = OptionParser() + p.add_option('-c', '--config', action='append', dest='config', + help='specify config file(s)') + p.add_option('-d', action='store_true', dest='daemonize', + help='run the server as a daemon') + p.add_option('-e', '--environment', dest='environment', default=None, + help='apply the given config environment') + p.add_option('-f', action='store_true', dest='fastcgi', + help='start a fastcgi server instead of the default HTTP ' + 'server') + p.add_option('-s', action='store_true', dest='scgi', + help='start a scgi server instead of the default HTTP server') + p.add_option('-x', action='store_true', dest='cgi', + help='start a cgi server instead of the default HTTP server') + p.add_option('-i', '--import', action='append', dest='imports', + help='specify modules to import') + p.add_option('-p', '--pidfile', dest='pidfile', default=None, + help='store the process id in the given file') + p.add_option('-P', '--Path', action='append', dest='Path', + help='add the given paths to sys.path') + options, args = p.parse_args() + + if options.Path: + for p in options.Path: + sys.path.insert(0, p) + + start(options.config, options.daemonize, + options.environment, options.fastcgi, options.scgi, + options.pidfile, options.imports, options.cgi) diff --git a/resources/lib/cherrypy/favicon.ico b/resources/lib/cherrypy/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f0d7e61badad3f332cf1e663efb97c0b5be80f5e GIT binary patch literal 1406 zcmb`Hd05U_6vscWSJJB#$ugQ@jA;zAXDv%YAxlVP&ytAjTU10ymNpe4LY9=2_SI5K zC3>Y)vZNK!rhR_zJdtfiw$m?BBmX0|pFW;J|@s zYHBiQ&>#j69?Xy-Ll`=AD8q&gWBBmlj2JNjEiElZjvUFTQKJ|=dNgCkjA889v5Xrx z4sC61baZqWKYlzDCQM-B#EDFrGznc@T_#VSjGmqzQ>IK|>eQ)Bn>G!7eSHiJ446KB zIx}X>VCKx37#bQfYt}4g&z{YkIdhmhcP>UoM$DTxkNNZGvtYpjjE#+1xNspRCMGOe zw1~xv7h`H_%915ZSh{p6%a$!;`SRtgSh0eYD_62=)hf))%vim8HEY(aVeQ(rtXsDZ zb8~anuV0Uag#{ZnY+&QYjaXV*vT4&MHgDdHm6a7+wrpYR)~#&YwvFxEx3go%4tDO` z$*x_y*u8rke3QwOtB{embw6rwR)6;qO>=_vu z89aafoEI-%keQi@R4V1=%a>$jW%26OE3&h*$;rv#_3PK<=H`-@mq&hnK5yQT(K__B|98+X@Zp^73MZjvW!HsVT|~sc)O^ZGM)}O{A)-)@n=bmFdz? zRaLc>D=T%Qr>f`&r)>x2Kb1tH)^h|wLs?1GQ@HOR^ik=lq13yT$@Z=qojU#WZ-LIg Kbp8+jKgeIP@z;_7 literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/lib/__init__.py b/resources/lib/cherrypy/lib/__init__.py new file mode 100644 index 0000000..f815f76 --- /dev/null +++ b/resources/lib/cherrypy/lib/__init__.py @@ -0,0 +1,96 @@ +"""CherryPy Library.""" + + +def is_iterator(obj): + """Detect if the object provided implements the iterator protocol. + + (i.e. like a generator). + + This will return False for objects which are iterable, + but not iterators themselves. + """ + from types import GeneratorType + if isinstance(obj, GeneratorType): + return True + elif not hasattr(obj, '__iter__'): + return False + else: + # Types which implement the protocol must return themselves when + # invoking 'iter' upon them. + return iter(obj) is obj + + +def is_closable_iterator(obj): + """Detect if the given object is both closable and iterator.""" + # Not an iterator. + if not is_iterator(obj): + return False + + # A generator - the easiest thing to deal with. + import inspect + if inspect.isgenerator(obj): + return True + + # A custom iterator. Look for a close method... + if not (hasattr(obj, 'close') and callable(obj.close)): + return False + + # ... which doesn't require any arguments. + try: + inspect.getcallargs(obj.close) + except TypeError: + return False + else: + return True + + +class file_generator(object): + """Yield the given input (a file object) in chunks (default 64k). + + (Core) + """ + + def __init__(self, input, chunkSize=65536): + """Initialize file_generator with file ``input`` for chunked access.""" + self.input = input + self.chunkSize = chunkSize + + def __iter__(self): + """Return iterator.""" + return self + + def __next__(self): + """Return next chunk of file.""" + chunk = self.input.read(self.chunkSize) + if chunk: + return chunk + else: + if hasattr(self.input, 'close'): + self.input.close() + raise StopIteration() + next = __next__ + + +def file_generator_limited(fileobj, count, chunk_size=65536): + """Yield the given file object in chunks. + + Stopps after `count` bytes has been emitted. + Default chunk size is 64kB. (Core) + """ + remaining = count + while remaining > 0: + chunk = fileobj.read(min(chunk_size, remaining)) + chunklen = len(chunk) + if chunklen == 0: + return + remaining -= chunklen + yield chunk + + +def set_vary_header(response, header_name): + """Add a Vary header to a response.""" + varies = response.headers.get('Vary', '') + varies = [x.strip() for x in varies.split(',') if x.strip()] + if header_name not in varies: + varies.append(header_name) + response.headers['Vary'] = ', '.join(varies) diff --git a/resources/lib/cherrypy/lib/__pycache__/__init__.cpython-37.opt-1.pyc b/resources/lib/cherrypy/lib/__pycache__/__init__.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc767992c2d79a0de80718693a6203f9e84e019e GIT binary patch literal 2789 zcmZuz-E!Qv6$VHyms&}V(x^`2b~>2$qS_hHw#lTP#N*M#mg_WaN9{yzesvV`a?juQTyyePA*(8{`~&(iNbEXgg^3Q*@-HU=x1IIdeh78h#d- zdotW(E^L^L&H}q7JNSwkcE%falI+0%sOirC^He#NJE0GS8!J&A9Me~As#jVnDfFZ+ z)I^oe`f2Ty$y{Yfz*Tuw40=6*YfBH*Kot5!iA)@+av{~foP9plR(!9ELKx+yrWDVz z!YXlyeHQvKJJ$JFWJU?E;-FCX{l*7VC&~)uT`KlwOspziDLe2IKj8eV&)fAcmTI5R zYW2KL(v%jZ>9gH_r|mclT0@>|d)n&Kx~$Aq+Z|^%bI!C83HO6GghgKL-^{E)`mNk~ zIQpAa#*PX*A3d(?rKDXY?s>SB7N%Rx4kx*FKJa{6#KGRjs{ zlPfIO2cz7_w4V777+7kTrh|Icu4?PeCFc@`>>4_jtnnU?s6}QK-!~ADV8A0_j2Fi! zvUW{!L^hCN0W38999X#E=9eNzXPQ7t_cE(DMx|HzVe{Cq46b z&gdqYZ-F7~3q1AX`DZr~bEIFXa)}$n`Ji%Rkr$QC5JZucz|dvv2G0y3c=vhRp?TF$ z{Gr+|qEQ2NLpb3IJ0^x`o8(zh&_;&*F-Za1$-y8~hHB1G9m-oCKCvh?mdN>;Ro2LW9hb4@1zF zsLerTPizpmD-$BJiicgf0-vq6cMf%-(yPPG|MHzyh4hqK_)nKLkiL}(61?7pkgeFx3bNg8lIWew1fa*Ag^m_Pm! z*wa~|=OE$@ zDL3R9%FLGB!-5~v>T|}GR+6JdUh4zAfGF%7?mP}IJjKl21`ee(dkm_8UiZC3bOB>s z=ZyUWsTp>L^Lv+emg==zL{(IW-pvCK?%jUuoz_N1Wpd#<;((uQ*Y{jif9Z*&s}g{x z(}72Xj$4PM%tuPnbQ>~@L=xOV8i@i*CqHuLh4e0b_7kk=Cw_-)qSCbOrRk)SQ-C*Z z*V6RmG%L!*OgBwsm8Yq>OGc89%sq5%M7v!x$%mh)Xx>Hl8#GH_NfZUWP7)<6e*6at z?iGOP+#R%>r2l}f;iuv2?Ix7`Br;DLcFr6JoWnDULWGhRo$=F7)4_aTJ|>ShVbl4P zorX>HHFka(e}lB>YR-Ow@)bC2d%!vLClyG6yvXu_bM7&+nAd zPVI_OlMLi3kIaWyH%$B-)?IVAPkMK6k|`}TY*g}(`EO_yV z$u7K~%*A7g1R}o1|BM&|=Wwg5Fn}^yqlT(M8_`^Qv(%9_duaCO=#Id_7n{Dku?uD* zlrKF08N5V^@{YjIK*u_xYb2;(@Y|NZzU7~{y+?(%F0Ur__6ypUP9C%QudH(EE2vKw zrVSny70Qj>#>-OvEx$hx?hE>-=q{ve8=@>hWnF03##vpfQnt}?rAuVPWO=f7bs?S- ze}*yCX#9}eN{;W`vS3KoQAimWyopHlxS=dWTorisyXF^o_&VxqRvQC5sHL7%KCE8; cnw~2@w#lOfwYMp)dMzl0?{H(X}KS4{sj{pDw literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/lib/__pycache__/auth_basic.cpython-37.opt-1.pyc b/resources/lib/cherrypy/lib/__pycache__/auth_basic.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..61175aa470e11ae2fb40ee7c3f628691e9f57fe3 GIT binary patch literal 3929 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 HeB%EB$5uSn literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/lib/__pycache__/encoding.cpython-37.opt-1.pyc b/resources/lib/cherrypy/lib/__pycache__/encoding.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..63e3015d35372930cb8d79d93c1e81d7f63758bc GIT binary patch literal 9995 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 literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/lib/__pycache__/locking.cpython-37.opt-1.pyc b/resources/lib/cherrypy/lib/__pycache__/locking.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0f71f4bead76b3007e7f24ea2addb600df6a4bfb GIT binary patch literal 2106 zcmaJ?&5qkP5GM6w$=;uCn{5&lMZrT0L}=j$2#Ug6CtDN=Tp&#@f(xxyq+{*Ml0s71 zuHaKQPtj9y?3?r%bnU6H&{Jnfdu<0z36Ro|)NsCUzM;N9I2aIUSHFMs%by-0f1$B% zV3QGa^%Mq9I4wv_ejwc8?i<2gL2umH0o&s~*uJq{uzNfJI}jeM`(TH>4|d!VN$haSoi`+QxeH%;-Tp+p7V`10buNXiSWt)9JG+UF7A<>641*)0Fe7 zRMWa>UgqU6ZFpV{RbA+@;pnmU{`Zdvb1CdTdMP_G<(@RRjf{x5`1VpWrh(52cOtY=1_=Bx+KXef9=Du z6zDeHKGE`~7#x1KIp>T$c=3G9IBdu0q6s8^uvv~Ai5$s29FPz@Ac4q!UgkPU4#2Xx za@s=!e7IiP2^haO0e=E7OQOjIc}w3x==aDCikr4S8kES{Goc#^oNOa7oLLwwooS%* zOcZGi?3`Jalw}FOkDM-CgX(&X&dTZv=G?4MpfUT^`+$7o^y6DJe!FMUnE9Qmx#j){ zcHaUa7tW=RKqiqZkrjD}1Ckd}OXMzxMxFOzc`fw=d!Pl$Cs8N^^bq3EhoK9SgjZRT z$bGc-vI5kx5PDv5iGhDu?kE!UpwL(7dNlO*1BtU4^xKBl!3erSV7sCRU~aef6lxWN ztQy^(o}gqiQ5(ft5i=}imQ_u`Sy^dz4l>MMHE{26c%M~rElY5~Y9aFqV0!Ptl&Fxc z^cdDm)#4rZImU+>fT~4%2LR5GpeqK$E&#lJW6#!X?DHM~3M`=UTx6F}uaAuWo(NGh zEmOcz&7fRu;X-|K#)TG{X8DX+Wg-YH(pAE2kz%yi@QHm4U403IEz+eU?`!(jSvv2q zXx%c@-VM}4;&Nie<97okl!9v@$+^jqNp9zBygd`}|J;oHS5N`@~`=5ZGP0(M!%W_qw@-q#8740seSnopr4gN4FiAfu+^sq`a z+~igSkZktVgAI~MokFJXfln(_%w6ZbPNn*v>i8Nqt>Pb0haS@Q&UXB_mD2l92_XX? zu8B`!m7#z9Ntl`lB+f;Fsot(vonJt@4dEsYUH|b~?nZfL_ZI7zZS!~O!f);W=sVn+a literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/lib/__pycache__/reprconf.cpython-37.opt-1.pyc b/resources/lib/cherrypy/lib/__pycache__/reprconf.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..53fbc1a4d0005e222e54fcef0a755714a385c0e8 GIT binary patch literal 15457 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 literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/lib/__pycache__/sessions.cpython-37.opt-1.pyc b/resources/lib/cherrypy/lib/__pycache__/sessions.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7f751e3cf13711adf6ed316d750e4ea1a1d2739f GIT binary patch literal 26067 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

*of$PCbyum7rDB?d)K;5$$5fTpk=A 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 literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/lib/__pycache__/static.cpython-37.opt-1.pyc b/resources/lib/cherrypy/lib/__pycache__/static.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..94fa60291d8c99497023682574c620b750fdcb2c GIT binary patch literal 9431 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 qqF~mZ{3k!!eO= +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 +"""HTTP Basic Authentication tool. + +This module provides a CherryPy 3.x tool which implements +the server-side of HTTP Basic Access Authentication, as described in +:rfc:`2617`. + +Example usage, using the built-in checkpassword_dict function which uses a dict +as the credentials store:: + + userpassdict = {'bird' : 'bebop', 'ornette' : 'wayout'} + checkpassword = cherrypy.lib.auth_basic.checkpassword_dict(userpassdict) + basic_auth = {'tools.auth_basic.on': True, + 'tools.auth_basic.realm': 'earth', + 'tools.auth_basic.checkpassword': checkpassword, + 'tools.auth_basic.accept_charset': 'UTF-8', + } + app_config = { '/' : basic_auth } + +""" + +import binascii +import unicodedata +import base64 + +import cherrypy +from cherrypy._cpcompat import ntou, tonative + + +__author__ = 'visteya' +__date__ = 'April 2009' + + +def checkpassword_dict(user_password_dict): + """Returns a checkpassword function which checks credentials + against a dictionary of the form: {username : password}. + + If you want a simple dictionary-based authentication scheme, use + checkpassword_dict(my_credentials_dict) as the value for the + checkpassword argument to basic_auth(). + """ + def checkpassword(realm, user, password): + p = user_password_dict.get(user) + return p and p == password or False + + return checkpassword + + +def basic_auth(realm, checkpassword, debug=False, accept_charset='utf-8'): + """A CherryPy tool which hooks at before_handler to perform + HTTP Basic Access Authentication, as specified in :rfc:`2617` + and :rfc:`7617`. + + If the request has an 'authorization' header with a 'Basic' scheme, this + tool attempts to authenticate the credentials supplied in that header. If + the request has no 'authorization' header, or if it does but the scheme is + not 'Basic', or if authentication fails, the tool sends a 401 response with + a 'WWW-Authenticate' Basic header. + + realm + A string containing the authentication realm. + + checkpassword + A callable which checks the authentication credentials. + Its signature is checkpassword(realm, username, password). where + username and password are the values obtained from the request's + 'authorization' header. If authentication succeeds, checkpassword + returns True, else it returns False. + + """ + + fallback_charset = 'ISO-8859-1' + + if '"' in realm: + raise ValueError('Realm cannot contain the " (quote) character.') + request = cherrypy.serving.request + + auth_header = request.headers.get('authorization') + if auth_header is not None: + # split() error, base64.decodestring() error + msg = 'Bad Request' + with cherrypy.HTTPError.handle((ValueError, binascii.Error), 400, msg): + scheme, params = auth_header.split(' ', 1) + if scheme.lower() == 'basic': + charsets = accept_charset, fallback_charset + decoded_params = base64.b64decode(params.encode('ascii')) + decoded_params = _try_decode(decoded_params, charsets) + decoded_params = ntou(decoded_params) + decoded_params = unicodedata.normalize('NFC', decoded_params) + decoded_params = tonative(decoded_params) + username, password = decoded_params.split(':', 1) + if checkpassword(realm, username, password): + if debug: + cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC') + request.login = username + return # successful authentication + + charset = accept_charset.upper() + charset_declaration = ( + (', charset="%s"' % charset) + if charset != fallback_charset + else '' + ) + # Respond with 401 status and a WWW-Authenticate header + cherrypy.serving.response.headers['www-authenticate'] = ( + 'Basic realm="%s"%s' % (realm, charset_declaration) + ) + raise cherrypy.HTTPError( + 401, 'You are not authorized to access that resource') + + +def _try_decode(subject, charsets): + for charset in charsets[:-1]: + try: + return tonative(subject, charset) + except ValueError: + pass + return tonative(subject, charsets[-1]) diff --git a/resources/lib/cherrypy/lib/auth_digest.py b/resources/lib/cherrypy/lib/auth_digest.py new file mode 100644 index 0000000..9b4f55c --- /dev/null +++ b/resources/lib/cherrypy/lib/auth_digest.py @@ -0,0 +1,464 @@ +# This file is part of CherryPy +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 +"""HTTP Digest Authentication tool. + +An implementation of the server-side of HTTP Digest Access +Authentication, which is described in :rfc:`2617`. + +Example usage, using the built-in get_ha1_dict_plain function which uses a dict +of plaintext passwords as the credentials store:: + + userpassdict = {'alice' : '4x5istwelve'} + get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(userpassdict) + digest_auth = {'tools.auth_digest.on': True, + 'tools.auth_digest.realm': 'wonderland', + 'tools.auth_digest.get_ha1': get_ha1, + 'tools.auth_digest.key': 'a565c27146791cfb', + 'tools.auth_digest.accept_charset': 'UTF-8', + } + app_config = { '/' : digest_auth } +""" + +import time +import functools +from hashlib import md5 + +from six.moves.urllib.request import parse_http_list, parse_keqv_list + +import cherrypy +from cherrypy._cpcompat import ntob, tonative + + +__author__ = 'visteya' +__date__ = 'April 2009' + + +def md5_hex(s): + return md5(ntob(s, 'utf-8')).hexdigest() + + +qop_auth = 'auth' +qop_auth_int = 'auth-int' +valid_qops = (qop_auth, qop_auth_int) + +valid_algorithms = ('MD5', 'MD5-sess') + +FALLBACK_CHARSET = 'ISO-8859-1' +DEFAULT_CHARSET = 'UTF-8' + + +def TRACE(msg): + cherrypy.log(msg, context='TOOLS.AUTH_DIGEST') + +# Three helper functions for users of the tool, providing three variants +# of get_ha1() functions for three different kinds of credential stores. + + +def get_ha1_dict_plain(user_password_dict): + """Returns a get_ha1 function which obtains a plaintext password from a + dictionary of the form: {username : password}. + + If you want a simple dictionary-based authentication scheme, with plaintext + passwords, use get_ha1_dict_plain(my_userpass_dict) as the value for the + get_ha1 argument to digest_auth(). + """ + def get_ha1(realm, username): + password = user_password_dict.get(username) + if password: + return md5_hex('%s:%s:%s' % (username, realm, password)) + return None + + return get_ha1 + + +def get_ha1_dict(user_ha1_dict): + """Returns a get_ha1 function which obtains a HA1 password hash from a + dictionary of the form: {username : HA1}. + + If you want a dictionary-based authentication scheme, but with + pre-computed HA1 hashes instead of plain-text passwords, use + get_ha1_dict(my_userha1_dict) as the value for the get_ha1 + argument to digest_auth(). + """ + def get_ha1(realm, username): + return user_ha1_dict.get(username) + + return get_ha1 + + +def get_ha1_file_htdigest(filename): + """Returns a get_ha1 function which obtains a HA1 password hash from a + flat file with lines of the same format as that produced by the Apache + htdigest utility. For example, for realm 'wonderland', username 'alice', + and password '4x5istwelve', the htdigest line would be:: + + alice:wonderland:3238cdfe91a8b2ed8e39646921a02d4c + + If you want to use an Apache htdigest file as the credentials store, + then use get_ha1_file_htdigest(my_htdigest_file) as the value for the + get_ha1 argument to digest_auth(). It is recommended that the filename + argument be an absolute path, to avoid problems. + """ + def get_ha1(realm, username): + result = None + f = open(filename, 'r') + for line in f: + u, r, ha1 = line.rstrip().split(':') + if u == username and r == realm: + result = ha1 + break + f.close() + return result + + return get_ha1 + + +def synthesize_nonce(s, key, timestamp=None): + """Synthesize a nonce value which resists spoofing and can be checked + for staleness. Returns a string suitable as the value for 'nonce' in + the www-authenticate header. + + s + A string related to the resource, such as the hostname of the server. + + key + A secret string known only to the server. + + timestamp + An integer seconds-since-the-epoch timestamp + + """ + if timestamp is None: + timestamp = int(time.time()) + h = md5_hex('%s:%s:%s' % (timestamp, s, key)) + nonce = '%s:%s' % (timestamp, h) + return nonce + + +def H(s): + """The hash function H""" + return md5_hex(s) + + +def _try_decode_header(header, charset): + global FALLBACK_CHARSET + + for enc in (charset, FALLBACK_CHARSET): + try: + return tonative(ntob(tonative(header, 'latin1'), 'latin1'), enc) + except ValueError as ve: + last_err = ve + else: + raise last_err + + +class HttpDigestAuthorization(object): + """ + Parses a Digest Authorization header and performs + re-calculation of the digest. + """ + + scheme = 'digest' + + def errmsg(self, s): + return 'Digest Authorization header: %s' % s + + @classmethod + def matches(cls, header): + scheme, _, _ = header.partition(' ') + return scheme.lower() == cls.scheme + + def __init__( + self, auth_header, http_method, + debug=False, accept_charset=DEFAULT_CHARSET[:], + ): + self.http_method = http_method + self.debug = debug + + if not self.matches(auth_header): + raise ValueError('Authorization scheme is not "Digest"') + + self.auth_header = _try_decode_header(auth_header, accept_charset) + + scheme, params = self.auth_header.split(' ', 1) + + # make a dict of the params + items = parse_http_list(params) + paramsd = parse_keqv_list(items) + + self.realm = paramsd.get('realm') + self.username = paramsd.get('username') + self.nonce = paramsd.get('nonce') + self.uri = paramsd.get('uri') + self.method = paramsd.get('method') + self.response = paramsd.get('response') # the response digest + self.algorithm = paramsd.get('algorithm', 'MD5').upper() + self.cnonce = paramsd.get('cnonce') + self.opaque = paramsd.get('opaque') + self.qop = paramsd.get('qop') # qop + self.nc = paramsd.get('nc') # nonce count + + # perform some correctness checks + if self.algorithm not in valid_algorithms: + raise ValueError( + self.errmsg("Unsupported value for algorithm: '%s'" % + self.algorithm)) + + has_reqd = ( + self.username and + self.realm and + self.nonce and + self.uri and + self.response + ) + if not has_reqd: + raise ValueError( + self.errmsg('Not all required parameters are present.')) + + if self.qop: + if self.qop not in valid_qops: + raise ValueError( + self.errmsg("Unsupported value for qop: '%s'" % self.qop)) + if not (self.cnonce and self.nc): + raise ValueError( + self.errmsg('If qop is sent then ' + 'cnonce and nc MUST be present')) + else: + if self.cnonce or self.nc: + raise ValueError( + self.errmsg('If qop is not sent, ' + 'neither cnonce nor nc can be present')) + + def __str__(self): + return 'authorization : %s' % self.auth_header + + def validate_nonce(self, s, key): + """Validate the nonce. + Returns True if nonce was generated by synthesize_nonce() and the + timestamp is not spoofed, else returns False. + + s + A string related to the resource, such as the hostname of + the server. + + key + A secret string known only to the server. + + Both s and key must be the same values which were used to synthesize + the nonce we are trying to validate. + """ + try: + timestamp, hashpart = self.nonce.split(':', 1) + s_timestamp, s_hashpart = synthesize_nonce( + s, key, timestamp).split(':', 1) + is_valid = s_hashpart == hashpart + if self.debug: + TRACE('validate_nonce: %s' % is_valid) + return is_valid + except ValueError: # split() error + pass + return False + + def is_nonce_stale(self, max_age_seconds=600): + """Returns True if a validated nonce is stale. The nonce contains a + timestamp in plaintext and also a secure hash of the timestamp. + You should first validate the nonce to ensure the plaintext + timestamp is not spoofed. + """ + try: + timestamp, hashpart = self.nonce.split(':', 1) + if int(timestamp) + max_age_seconds > int(time.time()): + return False + except ValueError: # int() error + pass + if self.debug: + TRACE('nonce is stale') + return True + + def HA2(self, entity_body=''): + """Returns the H(A2) string. See :rfc:`2617` section 3.2.2.3.""" + # RFC 2617 3.2.2.3 + # If the "qop" directive's value is "auth" or is unspecified, + # then A2 is: + # A2 = method ":" digest-uri-value + # + # If the "qop" value is "auth-int", then A2 is: + # A2 = method ":" digest-uri-value ":" H(entity-body) + if self.qop is None or self.qop == 'auth': + a2 = '%s:%s' % (self.http_method, self.uri) + elif self.qop == 'auth-int': + a2 = '%s:%s:%s' % (self.http_method, self.uri, H(entity_body)) + else: + # in theory, this should never happen, since I validate qop in + # __init__() + raise ValueError(self.errmsg('Unrecognized value for qop!')) + return H(a2) + + def request_digest(self, ha1, entity_body=''): + """Calculates the Request-Digest. See :rfc:`2617` section 3.2.2.1. + + ha1 + The HA1 string obtained from the credentials store. + + entity_body + If 'qop' is set to 'auth-int', then A2 includes a hash + of the "entity body". The entity body is the part of the + message which follows the HTTP headers. See :rfc:`2617` section + 4.3. This refers to the entity the user agent sent in the + request which has the Authorization header. Typically GET + requests don't have an entity, and POST requests do. + + """ + ha2 = self.HA2(entity_body) + # Request-Digest -- RFC 2617 3.2.2.1 + if self.qop: + req = '%s:%s:%s:%s:%s' % ( + self.nonce, self.nc, self.cnonce, self.qop, ha2) + else: + req = '%s:%s' % (self.nonce, ha2) + + # RFC 2617 3.2.2.2 + # + # If the "algorithm" directive's value is "MD5" or is unspecified, + # then A1 is: + # A1 = unq(username-value) ":" unq(realm-value) ":" passwd + # + # If the "algorithm" directive's value is "MD5-sess", then A1 is + # calculated only once - on the first request by the client following + # receipt of a WWW-Authenticate challenge from the server. + # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd ) + # ":" unq(nonce-value) ":" unq(cnonce-value) + if self.algorithm == 'MD5-sess': + ha1 = H('%s:%s:%s' % (ha1, self.nonce, self.cnonce)) + + digest = H('%s:%s' % (ha1, req)) + return digest + + +def _get_charset_declaration(charset): + global FALLBACK_CHARSET + charset = charset.upper() + return ( + (', charset="%s"' % charset) + if charset != FALLBACK_CHARSET + else '' + ) + + +def www_authenticate( + realm, key, algorithm='MD5', nonce=None, qop=qop_auth, + stale=False, accept_charset=DEFAULT_CHARSET[:], +): + """Constructs a WWW-Authenticate header for Digest authentication.""" + if qop not in valid_qops: + raise ValueError("Unsupported value for qop: '%s'" % qop) + if algorithm not in valid_algorithms: + raise ValueError("Unsupported value for algorithm: '%s'" % algorithm) + + HEADER_PATTERN = ( + 'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"%s%s' + ) + + if nonce is None: + nonce = synthesize_nonce(realm, key) + + stale_param = ', stale="true"' if stale else '' + + charset_declaration = _get_charset_declaration(accept_charset) + + return HEADER_PATTERN % ( + realm, nonce, algorithm, qop, stale_param, charset_declaration, + ) + + +def digest_auth(realm, get_ha1, key, debug=False, accept_charset='utf-8'): + """A CherryPy tool that hooks at before_handler to perform + HTTP Digest Access Authentication, as specified in :rfc:`2617`. + + If the request has an 'authorization' header with a 'Digest' scheme, + this tool authenticates the credentials supplied in that header. + If the request has no 'authorization' header, or if it does but the + scheme is not "Digest", or if authentication fails, the tool sends + a 401 response with a 'WWW-Authenticate' Digest header. + + realm + A string containing the authentication realm. + + get_ha1 + A callable that looks up a username in a credentials store + and returns the HA1 string, which is defined in the RFC to be + MD5(username : realm : password). The function's signature is: + ``get_ha1(realm, username)`` + where username is obtained from the request's 'authorization' header. + If username is not found in the credentials store, get_ha1() returns + None. + + key + A secret string known only to the server, used in the synthesis + of nonces. + + """ + request = cherrypy.serving.request + + auth_header = request.headers.get('authorization') + + respond_401 = functools.partial( + _respond_401, realm, key, accept_charset, debug) + + if not HttpDigestAuthorization.matches(auth_header or ''): + respond_401() + + msg = 'The Authorization header could not be parsed.' + with cherrypy.HTTPError.handle(ValueError, 400, msg): + auth = HttpDigestAuthorization( + auth_header, request.method, + debug=debug, accept_charset=accept_charset, + ) + + if debug: + TRACE(str(auth)) + + if not auth.validate_nonce(realm, key): + respond_401() + + ha1 = get_ha1(realm, auth.username) + + if ha1 is None: + respond_401() + + # note that for request.body to be available we need to + # hook in at before_handler, not on_start_resource like + # 3.1.x digest_auth does. + digest = auth.request_digest(ha1, entity_body=request.body) + if digest != auth.response: + respond_401() + + # authenticated + if debug: + TRACE('digest matches auth.response') + # Now check if nonce is stale. + # The choice of ten minutes' lifetime for nonce is somewhat + # arbitrary + if auth.is_nonce_stale(max_age_seconds=600): + respond_401(stale=True) + + request.login = auth.username + if debug: + TRACE('authentication of %s successful' % auth.username) + + +def _respond_401(realm, key, accept_charset, debug, **kwargs): + """ + Respond with 401 status and a WWW-Authenticate header + """ + header = www_authenticate( + realm, key, + accept_charset=accept_charset, + **kwargs + ) + if debug: + TRACE(header) + cherrypy.serving.response.headers['WWW-Authenticate'] = header + raise cherrypy.HTTPError( + 401, 'You are not authorized to access that resource') diff --git a/resources/lib/cherrypy/lib/caching.py b/resources/lib/cherrypy/lib/caching.py new file mode 100644 index 0000000..1673b3c --- /dev/null +++ b/resources/lib/cherrypy/lib/caching.py @@ -0,0 +1,482 @@ +""" +CherryPy implements a simple caching system as a pluggable Tool. This tool +tries to be an (in-process) HTTP/1.1-compliant cache. It's not quite there +yet, but it's probably good enough for most sites. + +In general, GET responses are cached (along with selecting headers) and, if +another request arrives for the same resource, the caching Tool will return 304 +Not Modified if possible, or serve the cached response otherwise. It also sets +request.cached to True if serving a cached representation, and sets +request.cacheable to False (so it doesn't get cached again). + +If POST, PUT, or DELETE requests are made for a cached resource, they +invalidate (delete) any cached response. + +Usage +===== + +Configuration file example:: + + [/] + tools.caching.on = True + tools.caching.delay = 3600 + +You may use a class other than the default +:class:`MemoryCache` by supplying the config +entry ``cache_class``; supply the full dotted name of the replacement class +as the config value. It must implement the basic methods ``get``, ``put``, +``delete``, and ``clear``. + +You may set any attribute, including overriding methods, on the cache +instance by providing them in config. The above sets the +:attr:`delay` attribute, for example. +""" + +import datetime +import sys +import threading +import time + +import six + +import cherrypy +from cherrypy.lib import cptools, httputil +from cherrypy._cpcompat import Event + + +class Cache(object): + + """Base class for Cache implementations.""" + + def get(self): + """Return the current variant if in the cache, else None.""" + raise NotImplementedError + + def put(self, obj, size): + """Store the current variant in the cache.""" + raise NotImplementedError + + def delete(self): + """Remove ALL cached variants of the current resource.""" + raise NotImplementedError + + def clear(self): + """Reset the cache to its initial, empty state.""" + raise NotImplementedError + + +# ------------------------------ Memory Cache ------------------------------- # +class AntiStampedeCache(dict): + + """A storage system for cached items which reduces stampede collisions.""" + + def wait(self, key, timeout=5, debug=False): + """Return the cached value for the given key, or None. + + If timeout is not None, and the value is already + being calculated by another thread, wait until the given timeout has + elapsed. If the value is available before the timeout expires, it is + returned. If not, None is returned, and a sentinel placed in the cache + to signal other threads to wait. + + If timeout is None, no waiting is performed nor sentinels used. + """ + value = self.get(key) + if isinstance(value, Event): + if timeout is None: + # Ignore the other thread and recalc it ourselves. + if debug: + cherrypy.log('No timeout', 'TOOLS.CACHING') + return None + + # Wait until it's done or times out. + if debug: + cherrypy.log('Waiting up to %s seconds' % + timeout, 'TOOLS.CACHING') + value.wait(timeout) + if value.result is not None: + # The other thread finished its calculation. Use it. + if debug: + cherrypy.log('Result!', 'TOOLS.CACHING') + return value.result + # Timed out. Stick an Event in the slot so other threads wait + # on this one to finish calculating the value. + if debug: + cherrypy.log('Timed out', 'TOOLS.CACHING') + e = threading.Event() + e.result = None + dict.__setitem__(self, key, e) + + return None + elif value is None: + # Stick an Event in the slot so other threads wait + # on this one to finish calculating the value. + if debug: + cherrypy.log('Timed out', 'TOOLS.CACHING') + e = threading.Event() + e.result = None + dict.__setitem__(self, key, e) + return value + + def __setitem__(self, key, value): + """Set the cached value for the given key.""" + existing = self.get(key) + dict.__setitem__(self, key, value) + if isinstance(existing, Event): + # Set Event.result so other threads waiting on it have + # immediate access without needing to poll the cache again. + existing.result = value + existing.set() + + +class MemoryCache(Cache): + + """An in-memory cache for varying response content. + + Each key in self.store is a URI, and each value is an AntiStampedeCache. + The response for any given URI may vary based on the values of + "selecting request headers"; that is, those named in the Vary + response header. We assume the list of header names to be constant + for each URI throughout the lifetime of the application, and store + that list in ``self.store[uri].selecting_headers``. + + The items contained in ``self.store[uri]`` have keys which are tuples of + request header values (in the same order as the names in its + selecting_headers), and values which are the actual responses. + """ + + maxobjects = 1000 + """The maximum number of cached objects; defaults to 1000.""" + + maxobj_size = 100000 + """The maximum size of each cached object in bytes; defaults to 100 KB.""" + + maxsize = 10000000 + """The maximum size of the entire cache in bytes; defaults to 10 MB.""" + + delay = 600 + """Seconds until the cached content expires; defaults to 600 (10 minutes). + """ + + antistampede_timeout = 5 + """Seconds to wait for other threads to release a cache lock.""" + + expire_freq = 0.1 + """Seconds to sleep between cache expiration sweeps.""" + + debug = False + + def __init__(self): + self.clear() + + # Run self.expire_cache in a separate daemon thread. + t = threading.Thread(target=self.expire_cache, name='expire_cache') + self.expiration_thread = t + t.daemon = True + t.start() + + def clear(self): + """Reset the cache to its initial, empty state.""" + self.store = {} + self.expirations = {} + self.tot_puts = 0 + self.tot_gets = 0 + self.tot_hist = 0 + self.tot_expires = 0 + self.tot_non_modified = 0 + self.cursize = 0 + + def expire_cache(self): + """Continuously examine cached objects, expiring stale ones. + + This function is designed to be run in its own daemon thread, + referenced at ``self.expiration_thread``. + """ + # It's possible that "time" will be set to None + # arbitrarily, so we check "while time" to avoid exceptions. + # See tickets #99 and #180 for more information. + while time: + now = time.time() + # Must make a copy of expirations so it doesn't change size + # during iteration + items = list(six.iteritems(self.expirations)) + for expiration_time, objects in items: + if expiration_time <= now: + for obj_size, uri, sel_header_values in objects: + try: + del self.store[uri][tuple(sel_header_values)] + self.tot_expires += 1 + self.cursize -= obj_size + except KeyError: + # the key may have been deleted elsewhere + pass + del self.expirations[expiration_time] + time.sleep(self.expire_freq) + + def get(self): + """Return the current variant if in the cache, else None.""" + request = cherrypy.serving.request + self.tot_gets += 1 + + uri = cherrypy.url(qs=request.query_string) + uricache = self.store.get(uri) + if uricache is None: + return None + + header_values = [request.headers.get(h, '') + for h in uricache.selecting_headers] + variant = uricache.wait(key=tuple(sorted(header_values)), + timeout=self.antistampede_timeout, + debug=self.debug) + if variant is not None: + self.tot_hist += 1 + return variant + + def put(self, variant, size): + """Store the current variant in the cache.""" + request = cherrypy.serving.request + response = cherrypy.serving.response + + uri = cherrypy.url(qs=request.query_string) + uricache = self.store.get(uri) + if uricache is None: + uricache = AntiStampedeCache() + uricache.selecting_headers = [ + e.value for e in response.headers.elements('Vary')] + self.store[uri] = uricache + + if len(self.store) < self.maxobjects: + total_size = self.cursize + size + + # checks if there's space for the object + if (size < self.maxobj_size and total_size < self.maxsize): + # add to the expirations list + expiration_time = response.time + self.delay + bucket = self.expirations.setdefault(expiration_time, []) + bucket.append((size, uri, uricache.selecting_headers)) + + # add to the cache + header_values = [request.headers.get(h, '') + for h in uricache.selecting_headers] + uricache[tuple(sorted(header_values))] = variant + self.tot_puts += 1 + self.cursize = total_size + + def delete(self): + """Remove ALL cached variants of the current resource.""" + uri = cherrypy.url(qs=cherrypy.serving.request.query_string) + self.store.pop(uri, None) + + +def get(invalid_methods=('POST', 'PUT', 'DELETE'), debug=False, **kwargs): + """Try to obtain cached output. If fresh enough, raise HTTPError(304). + + If POST, PUT, or DELETE: + * invalidates (deletes) any cached response for this resource + * sets request.cached = False + * sets request.cacheable = False + + else if a cached copy exists: + * sets request.cached = True + * sets request.cacheable = False + * sets response.headers to the cached values + * checks the cached Last-Modified response header against the + current If-(Un)Modified-Since request headers; raises 304 + if necessary. + * sets response.status and response.body to the cached values + * returns True + + otherwise: + * sets request.cached = False + * sets request.cacheable = True + * returns False + """ + request = cherrypy.serving.request + response = cherrypy.serving.response + + if not hasattr(cherrypy, '_cache'): + # Make a process-wide Cache object. + cherrypy._cache = kwargs.pop('cache_class', MemoryCache)() + + # Take all remaining kwargs and set them on the Cache object. + for k, v in kwargs.items(): + setattr(cherrypy._cache, k, v) + cherrypy._cache.debug = debug + + # POST, PUT, DELETE should invalidate (delete) the cached copy. + # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10. + if request.method in invalid_methods: + if debug: + cherrypy.log('request.method %r in invalid_methods %r' % + (request.method, invalid_methods), 'TOOLS.CACHING') + cherrypy._cache.delete() + request.cached = False + request.cacheable = False + return False + + if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]: + request.cached = False + request.cacheable = True + return False + + cache_data = cherrypy._cache.get() + request.cached = bool(cache_data) + request.cacheable = not request.cached + if request.cached: + # Serve the cached copy. + max_age = cherrypy._cache.delay + for v in [e.value for e in request.headers.elements('Cache-Control')]: + atoms = v.split('=', 1) + directive = atoms.pop(0) + if directive == 'max-age': + if len(atoms) != 1 or not atoms[0].isdigit(): + raise cherrypy.HTTPError( + 400, 'Invalid Cache-Control header') + max_age = int(atoms[0]) + break + elif directive == 'no-cache': + if debug: + cherrypy.log( + 'Ignoring cache due to Cache-Control: no-cache', + 'TOOLS.CACHING') + request.cached = False + request.cacheable = True + return False + + if debug: + cherrypy.log('Reading response from cache', 'TOOLS.CACHING') + s, h, b, create_time = cache_data + age = int(response.time - create_time) + if (age > max_age): + if debug: + cherrypy.log('Ignoring cache due to age > %d' % max_age, + 'TOOLS.CACHING') + request.cached = False + request.cacheable = True + return False + + # Copy the response headers. See + # https://github.com/cherrypy/cherrypy/issues/721. + response.headers = rh = httputil.HeaderMap() + for k in h: + dict.__setitem__(rh, k, dict.__getitem__(h, k)) + + # Add the required Age header + response.headers['Age'] = str(age) + + try: + # Note that validate_since depends on a Last-Modified header; + # this was put into the cached copy, and should have been + # resurrected just above (response.headers = cache_data[1]). + cptools.validate_since() + except cherrypy.HTTPRedirect: + x = sys.exc_info()[1] + if x.status == 304: + cherrypy._cache.tot_non_modified += 1 + raise + + # serve it & get out from the request + response.status = s + response.body = b + else: + if debug: + cherrypy.log('request is not cached', 'TOOLS.CACHING') + return request.cached + + +def tee_output(): + """Tee response output to cache storage. Internal.""" + # Used by CachingTool by attaching to request.hooks + + request = cherrypy.serving.request + if 'no-store' in request.headers.values('Cache-Control'): + return + + def tee(body): + """Tee response.body into a list.""" + if ('no-cache' in response.headers.values('Pragma') or + 'no-store' in response.headers.values('Cache-Control')): + for chunk in body: + yield chunk + return + + output = [] + for chunk in body: + output.append(chunk) + yield chunk + + # Save the cache data, but only if the body isn't empty. + # e.g. a 304 Not Modified on a static file response will + # have an empty body. + # If the body is empty, delete the cache because it + # contains a stale Threading._Event object that will + # stall all consecutive requests until the _Event times + # out + body = b''.join(output) + if not body: + cherrypy._cache.delete() + else: + cherrypy._cache.put((response.status, response.headers or {}, + body, response.time), len(body)) + + response = cherrypy.serving.response + response.body = tee(response.body) + + +def expires(secs=0, force=False, debug=False): + """Tool for influencing cache mechanisms using the 'Expires' header. + + secs + Must be either an int or a datetime.timedelta, and indicates the + number of seconds between response.time and when the response should + expire. The 'Expires' header will be set to response.time + secs. + If secs is zero, the 'Expires' header is set one year in the past, and + the following "cache prevention" headers are also set: + + * Pragma: no-cache + * Cache-Control': no-cache, must-revalidate + + force + If False, the following headers are checked: + + * Etag + * Last-Modified + * Age + * Expires + + If any are already present, none of the above response headers are set. + + """ + + response = cherrypy.serving.response + headers = response.headers + + cacheable = False + if not force: + # some header names that indicate that the response can be cached + for indicator in ('Etag', 'Last-Modified', 'Age', 'Expires'): + if indicator in headers: + cacheable = True + break + + if not cacheable and not force: + if debug: + cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES') + else: + if debug: + cherrypy.log('request is cacheable', 'TOOLS.EXPIRES') + if isinstance(secs, datetime.timedelta): + secs = (86400 * secs.days) + secs.seconds + + if secs == 0: + if force or ('Pragma' not in headers): + headers['Pragma'] = 'no-cache' + if cherrypy.serving.request.protocol >= (1, 1): + if force or 'Cache-Control' not in headers: + headers['Cache-Control'] = 'no-cache, must-revalidate' + # Set an explicit Expires date in the past. + expiry = httputil.HTTPDate(1169942400.0) + else: + expiry = httputil.HTTPDate(response.time + secs) + if force or 'Expires' not in headers: + headers['Expires'] = expiry diff --git a/resources/lib/cherrypy/lib/covercp.py b/resources/lib/cherrypy/lib/covercp.py new file mode 100644 index 0000000..0bafca1 --- /dev/null +++ b/resources/lib/cherrypy/lib/covercp.py @@ -0,0 +1,391 @@ +"""Code-coverage tools for CherryPy. + +To use this module, or the coverage tools in the test suite, +you need to download 'coverage.py', either Gareth Rees' `original +implementation `_ +or Ned Batchelder's `enhanced version: +`_ + +To turn on coverage tracing, use the following code:: + + cherrypy.engine.subscribe('start', covercp.start) + +DO NOT subscribe anything on the 'start_thread' channel, as previously +recommended. Calling start once in the main thread should be sufficient +to start coverage on all threads. Calling start again in each thread +effectively clears any coverage data gathered up to that point. + +Run your code, then use the ``covercp.serve()`` function to browse the +results in a web browser. If you run this module from the command line, +it will call ``serve()`` for you. +""" + +import re +import sys +import cgi +import os +import os.path + +from six.moves import urllib + +import cherrypy + + +localFile = os.path.join(os.path.dirname(__file__), 'coverage.cache') + +the_coverage = None +try: + from coverage import coverage + the_coverage = coverage(data_file=localFile) + + def start(): + the_coverage.start() +except ImportError: + # Setting the_coverage to None will raise errors + # that need to be trapped downstream. + the_coverage = None + + import warnings + warnings.warn( + 'No code coverage will be performed; ' + 'coverage.py could not be imported.') + + def start(): + pass +start.priority = 20 + +TEMPLATE_MENU = """ + + CherryPy Coverage Menu + + + +

CherryPy Coverage

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

%(name)s

+

%(fullpath)s

+

Coverage: %(pc)s%%

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

No modules covered.

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

%s

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

%s

+ + + """ % subtitle + for key in headers: + yield """ + """ % key + yield """ + + + """ + for subrow in subrows: + yield """ + """ + for value in subrow: + yield """ + """ % value + yield """ + """ + yield """ + +
%s
%s
""" + yield """ + + +""" + + def get_namespaces(self): + """Yield (title, scalars, collections) for each namespace.""" + s = extrapolate_statistics(logging.statistics) + for title, ns in sorted(s.items()): + scalars = [] + collections = [] + ns_fmt = self.formatting.get(title, {}) + for k, v in sorted(ns.items()): + fmt = ns_fmt.get(k, {}) + if isinstance(v, dict): + headers, subrows = self.get_dict_collection(v, fmt) + collections.append((k, ['ID'] + headers, subrows)) + elif isinstance(v, (list, tuple)): + headers, subrows = self.get_list_collection(v, fmt) + collections.append((k, headers, subrows)) + else: + format = ns_fmt.get(k, missing) + if format is None: + # Don't output this column. + continue + if hasattr(format, '__call__'): + v = format(v) + elif format is not missing: + v = format % v + scalars.append((k, v)) + yield title, scalars, collections + + def get_dict_collection(self, v, formatting): + """Return ([headers], [rows]) for the given collection.""" + # E.g., the 'Requests' dict. + headers = [] + vals = six.itervalues(v) + for record in vals: + for k3 in record: + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if k3 not in headers: + headers.append(k3) + headers.sort() + + subrows = [] + for k2, record in sorted(v.items()): + subrow = [k2] + for k3 in headers: + v3 = record.get(k3, '') + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if hasattr(format, '__call__'): + v3 = format(v3) + elif format is not missing: + v3 = format % v3 + subrow.append(v3) + subrows.append(subrow) + + return headers, subrows + + def get_list_collection(self, v, formatting): + """Return ([headers], [subrows]) for the given collection.""" + # E.g., the 'Slow Queries' list. + headers = [] + for record in v: + for k3 in record: + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if k3 not in headers: + headers.append(k3) + headers.sort() + + subrows = [] + for record in v: + subrow = [] + for k3 in headers: + v3 = record.get(k3, '') + format = formatting.get(k3, missing) + if format is None: + # Don't output this column. + continue + if hasattr(format, '__call__'): + v3 = format(v3) + elif format is not missing: + v3 = format % v3 + subrow.append(v3) + subrows.append(subrow) + + return headers, subrows + + if json is not None: + @cherrypy.expose + def data(self): + s = extrapolate_statistics(logging.statistics) + cherrypy.response.headers['Content-Type'] = 'application/json' + return json.dumps(s, sort_keys=True, indent=4) + + @cherrypy.expose + def pause(self, namespace): + logging.statistics.get(namespace, {})['Enabled'] = False + raise cherrypy.HTTPRedirect('./') + pause.cp_config = {'tools.allow.on': True, + 'tools.allow.methods': ['POST']} + + @cherrypy.expose + def resume(self, namespace): + logging.statistics.get(namespace, {})['Enabled'] = True + raise cherrypy.HTTPRedirect('./') + resume.cp_config = {'tools.allow.on': True, + 'tools.allow.methods': ['POST']} diff --git a/resources/lib/cherrypy/lib/cptools.py b/resources/lib/cherrypy/lib/cptools.py new file mode 100644 index 0000000..1c07963 --- /dev/null +++ b/resources/lib/cherrypy/lib/cptools.py @@ -0,0 +1,640 @@ +"""Functions for builtin CherryPy tools.""" + +import logging +import re +from hashlib import md5 + +import six +from six.moves import urllib + +import cherrypy +from cherrypy._cpcompat import text_or_bytes +from cherrypy.lib import httputil as _httputil +from cherrypy.lib import is_iterator + + +# Conditional HTTP request support # + +def validate_etags(autotags=False, debug=False): + """Validate the current ETag against If-Match, If-None-Match headers. + + If autotags is True, an ETag response-header value will be provided + from an MD5 hash of the response body (unless some other code has + already provided an ETag header). If False (the default), the ETag + will not be automatic. + + WARNING: the autotags feature is not designed for URL's which allow + methods other than GET. For example, if a POST to the same URL returns + no content, the automatic ETag will be incorrect, breaking a fundamental + use for entity tags in a possibly destructive fashion. Likewise, if you + raise 304 Not Modified, the response body will be empty, the ETag hash + will be incorrect, and your application will break. + See :rfc:`2616` Section 14.24. + """ + response = cherrypy.serving.response + + # Guard against being run twice. + if hasattr(response, 'ETag'): + return + + status, reason, msg = _httputil.valid_status(response.status) + + etag = response.headers.get('ETag') + + # Automatic ETag generation. See warning in docstring. + if etag: + if debug: + cherrypy.log('ETag already set: %s' % etag, 'TOOLS.ETAGS') + elif not autotags: + if debug: + cherrypy.log('Autotags off', 'TOOLS.ETAGS') + elif status != 200: + if debug: + cherrypy.log('Status not 200', 'TOOLS.ETAGS') + else: + etag = response.collapse_body() + etag = '"%s"' % md5(etag).hexdigest() + if debug: + cherrypy.log('Setting ETag: %s' % etag, 'TOOLS.ETAGS') + response.headers['ETag'] = etag + + response.ETag = etag + + # "If the request would, without the If-Match header field, result in + # anything other than a 2xx or 412 status, then the If-Match header + # MUST be ignored." + if debug: + cherrypy.log('Status: %s' % status, 'TOOLS.ETAGS') + if status >= 200 and status <= 299: + request = cherrypy.serving.request + + conditions = request.headers.elements('If-Match') or [] + conditions = [str(x) for x in conditions] + if debug: + cherrypy.log('If-Match conditions: %s' % repr(conditions), + 'TOOLS.ETAGS') + if conditions and not (conditions == ['*'] or etag in conditions): + raise cherrypy.HTTPError(412, 'If-Match failed: ETag %r did ' + 'not match %r' % (etag, conditions)) + + conditions = request.headers.elements('If-None-Match') or [] + conditions = [str(x) for x in conditions] + if debug: + cherrypy.log('If-None-Match conditions: %s' % repr(conditions), + 'TOOLS.ETAGS') + if conditions == ['*'] or etag in conditions: + if debug: + cherrypy.log('request.method: %s' % + request.method, 'TOOLS.ETAGS') + if request.method in ('GET', 'HEAD'): + raise cherrypy.HTTPRedirect([], 304) + else: + raise cherrypy.HTTPError(412, 'If-None-Match failed: ETag %r ' + 'matched %r' % (etag, conditions)) + + +def validate_since(): + """Validate the current Last-Modified against If-Modified-Since headers. + + If no code has set the Last-Modified response header, then no validation + will be performed. + """ + response = cherrypy.serving.response + lastmod = response.headers.get('Last-Modified') + if lastmod: + status, reason, msg = _httputil.valid_status(response.status) + + request = cherrypy.serving.request + + since = request.headers.get('If-Unmodified-Since') + if since and since != lastmod: + if (status >= 200 and status <= 299) or status == 412: + raise cherrypy.HTTPError(412) + + since = request.headers.get('If-Modified-Since') + if since and since == lastmod: + if (status >= 200 and status <= 299) or status == 304: + if request.method in ('GET', 'HEAD'): + raise cherrypy.HTTPRedirect([], 304) + else: + raise cherrypy.HTTPError(412) + + +# Tool code # + +def allow(methods=None, debug=False): + """Raise 405 if request.method not in methods (default ['GET', 'HEAD']). + + The given methods are case-insensitive, and may be in any order. + If only one method is allowed, you may supply a single string; + if more than one, supply a list of strings. + + Regardless of whether the current method is allowed or not, this + also emits an 'Allow' response header, containing the given methods. + """ + if not isinstance(methods, (tuple, list)): + methods = [methods] + methods = [m.upper() for m in methods if m] + if not methods: + methods = ['GET', 'HEAD'] + elif 'GET' in methods and 'HEAD' not in methods: + methods.append('HEAD') + + cherrypy.response.headers['Allow'] = ', '.join(methods) + if cherrypy.request.method not in methods: + if debug: + cherrypy.log('request.method %r not in methods %r' % + (cherrypy.request.method, methods), 'TOOLS.ALLOW') + raise cherrypy.HTTPError(405) + else: + if debug: + cherrypy.log('request.method %r in methods %r' % + (cherrypy.request.method, methods), 'TOOLS.ALLOW') + + +def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For', + scheme='X-Forwarded-Proto', debug=False): + """Change the base URL (scheme://host[:port][/path]). + + For running a CP server behind Apache, lighttpd, or other HTTP server. + + For Apache and lighttpd, you should leave the 'local' argument at the + default value of 'X-Forwarded-Host'. For Squid, you probably want to set + tools.proxy.local = 'Origin'. + + If you want the new request.base to include path info (not just the host), + you must explicitly set base to the full base path, and ALSO set 'local' + to '', so that the X-Forwarded-Host request header (which never includes + path info) does not override it. Regardless, the value for 'base' MUST + NOT end in a slash. + + cherrypy.request.remote.ip (the IP address of the client) will be + rewritten if the header specified by the 'remote' arg is valid. + By default, 'remote' is set to 'X-Forwarded-For'. If you do not + want to rewrite remote.ip, set the 'remote' arg to an empty string. + """ + + request = cherrypy.serving.request + + if scheme: + s = request.headers.get(scheme, None) + if debug: + cherrypy.log('Testing scheme %r:%r' % (scheme, s), 'TOOLS.PROXY') + if s == 'on' and 'ssl' in scheme.lower(): + # This handles e.g. webfaction's 'X-Forwarded-Ssl: on' header + scheme = 'https' + else: + # This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https' + scheme = s + if not scheme: + scheme = request.base[:request.base.find('://')] + + if local: + lbase = request.headers.get(local, None) + if debug: + cherrypy.log('Testing local %r:%r' % (local, lbase), 'TOOLS.PROXY') + if lbase is not None: + base = lbase.split(',')[0] + if not base: + default = urllib.parse.urlparse(request.base).netloc + base = request.headers.get('Host', default) + + if base.find('://') == -1: + # add http:// or https:// if needed + base = scheme + '://' + base + + request.base = base + + if remote: + xff = request.headers.get(remote) + if debug: + cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY') + if xff: + if remote == 'X-Forwarded-For': + # Grab the first IP in a comma-separated list. Ref #1268. + xff = next(ip.strip() for ip in xff.split(',')) + request.remote.ip = xff + + +def ignore_headers(headers=('Range',), debug=False): + """Delete request headers whose field names are included in 'headers'. + + This is a useful tool for working behind certain HTTP servers; + for example, Apache duplicates the work that CP does for 'Range' + headers, and will doubly-truncate the response. + """ + request = cherrypy.serving.request + for name in headers: + if name in request.headers: + if debug: + cherrypy.log('Ignoring request header %r' % name, + 'TOOLS.IGNORE_HEADERS') + del request.headers[name] + + +def response_headers(headers=None, debug=False): + """Set headers on the response.""" + if debug: + cherrypy.log('Setting response headers: %s' % repr(headers), + 'TOOLS.RESPONSE_HEADERS') + for name, value in (headers or []): + cherrypy.serving.response.headers[name] = value + + +response_headers.failsafe = True + + +def referer(pattern, accept=True, accept_missing=False, error=403, + message='Forbidden Referer header.', debug=False): + """Raise HTTPError if Referer header does/does not match the given pattern. + + pattern + A regular expression pattern to test against the Referer. + + accept + If True, the Referer must match the pattern; if False, + the Referer must NOT match the pattern. + + accept_missing + If True, permit requests with no Referer header. + + error + The HTTP error code to return to the client on failure. + + message + A string to include in the response body on failure. + + """ + try: + ref = cherrypy.serving.request.headers['Referer'] + match = bool(re.match(pattern, ref)) + if debug: + cherrypy.log('Referer %r matches %r' % (ref, pattern), + 'TOOLS.REFERER') + if accept == match: + return + except KeyError: + if debug: + cherrypy.log('No Referer header', 'TOOLS.REFERER') + if accept_missing: + return + + raise cherrypy.HTTPError(error, message) + + +class SessionAuth(object): + + """Assert that the user is logged in.""" + + session_key = 'username' + debug = False + + def check_username_and_password(self, username, password): + pass + + def anonymous(self): + """Provide a temporary user name for anonymous users.""" + pass + + def on_login(self, username): + pass + + def on_logout(self, username): + pass + + def on_check(self, username): + pass + + def login_screen(self, from_page='..', username='', error_msg='', + **kwargs): + return (six.text_type(""" +Message: %(error_msg)s +
+ Login: +
+ Password: +
+ +
+ +
+""") % vars()).encode('utf-8') + + def do_login(self, username, password, from_page='..', **kwargs): + """Login. May raise redirect, or return True if request handled.""" + response = cherrypy.serving.response + error_msg = self.check_username_and_password(username, password) + if error_msg: + body = self.login_screen(from_page, username, error_msg) + response.body = body + if 'Content-Length' in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers['Content-Length'] + return True + else: + cherrypy.serving.request.login = username + cherrypy.session[self.session_key] = username + self.on_login(username) + raise cherrypy.HTTPRedirect(from_page or '/') + + def do_logout(self, from_page='..', **kwargs): + """Logout. May raise redirect, or return True if request handled.""" + sess = cherrypy.session + username = sess.get(self.session_key) + sess[self.session_key] = None + if username: + cherrypy.serving.request.login = None + self.on_logout(username) + raise cherrypy.HTTPRedirect(from_page) + + def do_check(self): + """Assert username. Raise redirect, or return True if request handled. + """ + sess = cherrypy.session + request = cherrypy.serving.request + response = cherrypy.serving.response + + username = sess.get(self.session_key) + if not username: + sess[self.session_key] = username = self.anonymous() + self._debug_message('No session[username], trying anonymous') + if not username: + url = cherrypy.url(qs=request.query_string) + self._debug_message( + 'No username, routing to login_screen with from_page %(url)r', + locals(), + ) + response.body = self.login_screen(url) + if 'Content-Length' in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers['Content-Length'] + return True + self._debug_message('Setting request.login to %(username)r', locals()) + request.login = username + self.on_check(username) + + def _debug_message(self, template, context={}): + if not self.debug: + return + cherrypy.log(template % context, 'TOOLS.SESSAUTH') + + def run(self): + request = cherrypy.serving.request + response = cherrypy.serving.response + + path = request.path_info + if path.endswith('login_screen'): + self._debug_message('routing %(path)r to login_screen', locals()) + response.body = self.login_screen() + return True + elif path.endswith('do_login'): + if request.method != 'POST': + response.headers['Allow'] = 'POST' + self._debug_message('do_login requires POST') + raise cherrypy.HTTPError(405) + self._debug_message('routing %(path)r to do_login', locals()) + return self.do_login(**request.params) + elif path.endswith('do_logout'): + if request.method != 'POST': + response.headers['Allow'] = 'POST' + raise cherrypy.HTTPError(405) + self._debug_message('routing %(path)r to do_logout', locals()) + return self.do_logout(**request.params) + else: + self._debug_message('No special path, running do_check') + return self.do_check() + + +def session_auth(**kwargs): + sa = SessionAuth() + for k, v in kwargs.items(): + setattr(sa, k, v) + return sa.run() + + +session_auth.__doc__ = ( + """Session authentication hook. + + Any attribute of the SessionAuth class may be overridden via a keyword arg + to this function: + + """ + '\n'.join(['%s: %s' % (k, type(getattr(SessionAuth, k)).__name__) + for k in dir(SessionAuth) if not k.startswith('__')]) +) + + +def log_traceback(severity=logging.ERROR, debug=False): + """Write the last error's traceback to the cherrypy error log.""" + cherrypy.log('', 'HTTP', severity=severity, traceback=True) + + +def log_request_headers(debug=False): + """Write request headers to the cherrypy error log.""" + h = [' %s: %s' % (k, v) for k, v in cherrypy.serving.request.header_list] + cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), 'HTTP') + + +def log_hooks(debug=False): + """Write request.hooks to the cherrypy error log.""" + request = cherrypy.serving.request + + msg = [] + # Sort by the standard points if possible. + from cherrypy import _cprequest + points = _cprequest.hookpoints + for k in request.hooks.keys(): + if k not in points: + points.append(k) + + for k in points: + msg.append(' %s:' % k) + v = request.hooks.get(k, []) + v.sort() + for h in v: + msg.append(' %r' % h) + cherrypy.log('\nRequest Hooks for ' + cherrypy.url() + + ':\n' + '\n'.join(msg), 'HTTP') + + +def redirect(url='', internal=True, debug=False): + """Raise InternalRedirect or HTTPRedirect to the given url.""" + if debug: + cherrypy.log('Redirecting %sto: %s' % + ({True: 'internal ', False: ''}[internal], url), + 'TOOLS.REDIRECT') + if internal: + raise cherrypy.InternalRedirect(url) + else: + raise cherrypy.HTTPRedirect(url) + + +def trailing_slash(missing=True, extra=False, status=None, debug=False): + """Redirect if path_info has (missing|extra) trailing slash.""" + request = cherrypy.serving.request + pi = request.path_info + + if debug: + cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' % + (request.is_index, missing, extra, pi), + 'TOOLS.TRAILING_SLASH') + if request.is_index is True: + if missing: + if not pi.endswith('/'): + new_url = cherrypy.url(pi + '/', request.query_string) + raise cherrypy.HTTPRedirect(new_url, status=status or 301) + elif request.is_index is False: + if extra: + # If pi == '/', don't redirect to ''! + if pi.endswith('/') and pi != '/': + new_url = cherrypy.url(pi[:-1], request.query_string) + raise cherrypy.HTTPRedirect(new_url, status=status or 301) + + +def flatten(debug=False): + """Wrap response.body in a generator that recursively iterates over body. + + This allows cherrypy.response.body to consist of 'nested generators'; + that is, a set of generators that yield generators. + """ + def flattener(input): + numchunks = 0 + for x in input: + if not is_iterator(x): + numchunks += 1 + yield x + else: + for y in flattener(x): + numchunks += 1 + yield y + if debug: + cherrypy.log('Flattened %d chunks' % numchunks, 'TOOLS.FLATTEN') + response = cherrypy.serving.response + response.body = flattener(response.body) + + +def accept(media=None, debug=False): + """Return the client's preferred media-type (from the given Content-Types). + + If 'media' is None (the default), no test will be performed. + + If 'media' is provided, it should be the Content-Type value (as a string) + or values (as a list or tuple of strings) which the current resource + can emit. The client's acceptable media ranges (as declared in the + Accept request header) will be matched in order to these Content-Type + values; the first such string is returned. That is, the return value + will always be one of the strings provided in the 'media' arg (or None + if 'media' is None). + + If no match is found, then HTTPError 406 (Not Acceptable) is raised. + Note that most web browsers send */* as a (low-quality) acceptable + media range, which should match any Content-Type. In addition, "...if + no Accept header field is present, then it is assumed that the client + accepts all media types." + + Matching types are checked in order of client preference first, + and then in the order of the given 'media' values. + + Note that this function does not honor accept-params (other than "q"). + """ + if not media: + return + if isinstance(media, text_or_bytes): + media = [media] + request = cherrypy.serving.request + + # Parse the Accept request header, and try to match one + # of the requested media-ranges (in order of preference). + ranges = request.headers.elements('Accept') + if not ranges: + # Any media type is acceptable. + if debug: + cherrypy.log('No Accept header elements', 'TOOLS.ACCEPT') + return media[0] + else: + # Note that 'ranges' is sorted in order of preference + for element in ranges: + if element.qvalue > 0: + if element.value == '*/*': + # Matches any type or subtype + if debug: + cherrypy.log('Match due to */*', 'TOOLS.ACCEPT') + return media[0] + elif element.value.endswith('/*'): + # Matches any subtype + mtype = element.value[:-1] # Keep the slash + for m in media: + if m.startswith(mtype): + if debug: + cherrypy.log('Match due to %s' % element.value, + 'TOOLS.ACCEPT') + return m + else: + # Matches exact value + if element.value in media: + if debug: + cherrypy.log('Match due to %s' % element.value, + 'TOOLS.ACCEPT') + return element.value + + # No suitable media-range found. + ah = request.headers.get('Accept') + if ah is None: + msg = 'Your client did not send an Accept header.' + else: + msg = 'Your client sent this Accept header: %s.' % ah + msg += (' But this resource only emits these media types: %s.' % + ', '.join(media)) + raise cherrypy.HTTPError(406, msg) + + +class MonitoredHeaderMap(_httputil.HeaderMap): + + def transform_key(self, key): + self.accessed_headers.add(key) + return super(MonitoredHeaderMap, self).transform_key(key) + + def __init__(self): + self.accessed_headers = set() + super(MonitoredHeaderMap, self).__init__() + + +def autovary(ignore=None, debug=False): + """Auto-populate the Vary response header based on request.header access. + """ + request = cherrypy.serving.request + + req_h = request.headers + request.headers = MonitoredHeaderMap() + request.headers.update(req_h) + if ignore is None: + ignore = set(['Content-Disposition', 'Content-Length', 'Content-Type']) + + def set_response_header(): + resp_h = cherrypy.serving.response.headers + v = set([e.value for e in resp_h.elements('Vary')]) + if debug: + cherrypy.log( + 'Accessed headers: %s' % request.headers.accessed_headers, + 'TOOLS.AUTOVARY') + v = v.union(request.headers.accessed_headers) + v = v.difference(ignore) + v = list(v) + v.sort() + resp_h['Vary'] = ', '.join(v) + request.hooks.attach('before_finalize', set_response_header, 95) + + +def convert_params(exception=ValueError, error=400): + """Convert request params based on function annotations, with error handling. + + exception + Exception class to catch. + + status + The HTTP error code to return to the client on failure. + """ + request = cherrypy.serving.request + types = request.handler.callable.__annotations__ + with cherrypy.HTTPError.handle(exception, error): + for key in set(types).intersection(request.params): + request.params[key] = types[key](request.params[key]) diff --git a/resources/lib/cherrypy/lib/encoding.py b/resources/lib/cherrypy/lib/encoding.py new file mode 100644 index 0000000..3d001ca --- /dev/null +++ b/resources/lib/cherrypy/lib/encoding.py @@ -0,0 +1,436 @@ +import struct +import time +import io + +import six + +import cherrypy +from cherrypy._cpcompat import text_or_bytes +from cherrypy.lib import file_generator +from cherrypy.lib import is_closable_iterator +from cherrypy.lib import set_vary_header + + +def decode(encoding=None, default_encoding='utf-8'): + """Replace or extend the list of charsets used to decode a request entity. + + Either argument may be a single string or a list of strings. + + encoding + If not None, restricts the set of charsets attempted while decoding + a request entity to the given set (even if a different charset is + given in the Content-Type request header). + + default_encoding + Only in effect if the 'encoding' argument is not given. + If given, the set of charsets attempted while decoding a request + entity is *extended* with the given value(s). + + """ + body = cherrypy.request.body + if encoding is not None: + if not isinstance(encoding, list): + encoding = [encoding] + body.attempt_charsets = encoding + elif default_encoding: + if not isinstance(default_encoding, list): + default_encoding = [default_encoding] + body.attempt_charsets = body.attempt_charsets + default_encoding + + +class UTF8StreamEncoder: + def __init__(self, iterator): + self._iterator = iterator + + def __iter__(self): + return self + + def next(self): + return self.__next__() + + def __next__(self): + res = next(self._iterator) + if isinstance(res, six.text_type): + res = res.encode('utf-8') + return res + + def close(self): + if is_closable_iterator(self._iterator): + self._iterator.close() + + def __getattr__(self, attr): + if attr.startswith('__'): + raise AttributeError(self, attr) + return getattr(self._iterator, attr) + + +class ResponseEncoder: + + default_encoding = 'utf-8' + failmsg = 'Response body could not be encoded with %r.' + encoding = None + errors = 'strict' + text_only = True + add_charset = True + debug = False + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + self.attempted_charsets = set() + request = cherrypy.serving.request + if request.handler is not None: + # Replace request.handler with self + if self.debug: + cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE') + self.oldhandler = request.handler + request.handler = self + + def encode_stream(self, encoding): + """Encode a streaming response body. + + Use a generator wrapper, and just pray it works as the stream is + being written out. + """ + if encoding in self.attempted_charsets: + return False + self.attempted_charsets.add(encoding) + + def encoder(body): + for chunk in body: + if isinstance(chunk, six.text_type): + chunk = chunk.encode(encoding, self.errors) + yield chunk + self.body = encoder(self.body) + return True + + def encode_string(self, encoding): + """Encode a buffered response body.""" + if encoding in self.attempted_charsets: + return False + self.attempted_charsets.add(encoding) + body = [] + for chunk in self.body: + if isinstance(chunk, six.text_type): + try: + chunk = chunk.encode(encoding, self.errors) + except (LookupError, UnicodeError): + return False + body.append(chunk) + self.body = body + return True + + def find_acceptable_charset(self): + request = cherrypy.serving.request + response = cherrypy.serving.response + + if self.debug: + cherrypy.log('response.stream %r' % + response.stream, 'TOOLS.ENCODE') + if response.stream: + encoder = self.encode_stream + else: + encoder = self.encode_string + if 'Content-Length' in response.headers: + # Delete Content-Length header so finalize() recalcs it. + # Encoded strings may be of different lengths from their + # unicode equivalents, and even from each other. For example: + # >>> t = u"\u7007\u3040" + # >>> len(t) + # 2 + # >>> len(t.encode("UTF-8")) + # 6 + # >>> len(t.encode("utf7")) + # 8 + del response.headers['Content-Length'] + + # Parse the Accept-Charset request header, and try to provide one + # of the requested charsets (in order of user preference). + encs = request.headers.elements('Accept-Charset') + charsets = [enc.value.lower() for enc in encs] + if self.debug: + cherrypy.log('charsets %s' % repr(charsets), 'TOOLS.ENCODE') + + if self.encoding is not None: + # If specified, force this encoding to be used, or fail. + encoding = self.encoding.lower() + if self.debug: + cherrypy.log('Specified encoding %r' % + encoding, 'TOOLS.ENCODE') + if (not charsets) or '*' in charsets or encoding in charsets: + if self.debug: + cherrypy.log('Attempting encoding %r' % + encoding, 'TOOLS.ENCODE') + if encoder(encoding): + return encoding + else: + if not encs: + if self.debug: + cherrypy.log('Attempting default encoding %r' % + self.default_encoding, 'TOOLS.ENCODE') + # Any character-set is acceptable. + if encoder(self.default_encoding): + return self.default_encoding + else: + raise cherrypy.HTTPError(500, self.failmsg % + self.default_encoding) + else: + for element in encs: + if element.qvalue > 0: + if element.value == '*': + # Matches any charset. Try our default. + if self.debug: + cherrypy.log('Attempting default encoding due ' + 'to %r' % element, 'TOOLS.ENCODE') + if encoder(self.default_encoding): + return self.default_encoding + else: + encoding = element.value + if self.debug: + cherrypy.log('Attempting encoding %s (qvalue >' + '0)' % element, 'TOOLS.ENCODE') + if encoder(encoding): + return encoding + + if '*' not in charsets: + # If no "*" is present in an Accept-Charset field, then all + # character sets not explicitly mentioned get a quality + # value of 0, except for ISO-8859-1, which gets a quality + # value of 1 if not explicitly mentioned. + iso = 'iso-8859-1' + if iso not in charsets: + if self.debug: + cherrypy.log('Attempting ISO-8859-1 encoding', + 'TOOLS.ENCODE') + if encoder(iso): + return iso + + # No suitable encoding found. + ac = request.headers.get('Accept-Charset') + if ac is None: + msg = 'Your client did not send an Accept-Charset header.' + else: + msg = 'Your client sent this Accept-Charset header: %s.' % ac + _charsets = ', '.join(sorted(self.attempted_charsets)) + msg += ' We tried these charsets: %s.' % (_charsets,) + raise cherrypy.HTTPError(406, msg) + + def __call__(self, *args, **kwargs): + response = cherrypy.serving.response + self.body = self.oldhandler(*args, **kwargs) + + self.body = prepare_iter(self.body) + + ct = response.headers.elements('Content-Type') + if self.debug: + cherrypy.log('Content-Type: %r' % [str(h) + for h in ct], 'TOOLS.ENCODE') + if ct and self.add_charset: + ct = ct[0] + if self.text_only: + if ct.value.lower().startswith('text/'): + if self.debug: + cherrypy.log( + 'Content-Type %s starts with "text/"' % ct, + 'TOOLS.ENCODE') + do_find = True + else: + if self.debug: + cherrypy.log('Not finding because Content-Type %s ' + 'does not start with "text/"' % ct, + 'TOOLS.ENCODE') + do_find = False + else: + if self.debug: + cherrypy.log('Finding because not text_only', + 'TOOLS.ENCODE') + do_find = True + + if do_find: + # Set "charset=..." param on response Content-Type header + ct.params['charset'] = self.find_acceptable_charset() + if self.debug: + cherrypy.log('Setting Content-Type %s' % ct, + 'TOOLS.ENCODE') + response.headers['Content-Type'] = str(ct) + + return self.body + + +def prepare_iter(value): + """ + Ensure response body is iterable and resolves to False when empty. + """ + if isinstance(value, text_or_bytes): + # strings get wrapped in a list because iterating over a single + # item list is much faster than iterating over every character + # in a long string. + if value: + value = [value] + else: + # [''] doesn't evaluate to False, so replace it with []. + value = [] + # Don't use isinstance here; io.IOBase which has an ABC takes + # 1000 times as long as, say, isinstance(value, str) + elif hasattr(value, 'read'): + value = file_generator(value) + elif value is None: + value = [] + return value + + +# GZIP + + +def compress(body, compress_level): + """Compress 'body' at the given compress_level.""" + import zlib + + # See http://www.gzip.org/zlib/rfc-gzip.html + yield b'\x1f\x8b' # ID1 and ID2: gzip marker + yield b'\x08' # CM: compression method + yield b'\x00' # FLG: none set + # MTIME: 4 bytes + yield struct.pack(' 0 is present + * The 'identity' value is given with a qvalue > 0. + + """ + request = cherrypy.serving.request + response = cherrypy.serving.response + + set_vary_header(response, 'Accept-Encoding') + + if not response.body: + # Response body is empty (might be a 304 for instance) + if debug: + cherrypy.log('No response body', context='TOOLS.GZIP') + return + + # If returning cached content (which should already have been gzipped), + # don't re-zip. + if getattr(request, 'cached', False): + if debug: + cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP') + return + + acceptable = request.headers.elements('Accept-Encoding') + if not acceptable: + # If no Accept-Encoding field is present in a request, + # the server MAY assume that the client will accept any + # content coding. In this case, if "identity" is one of + # the available content-codings, then the server SHOULD use + # the "identity" content-coding, unless it has additional + # information that a different content-coding is meaningful + # to the client. + if debug: + cherrypy.log('No Accept-Encoding', context='TOOLS.GZIP') + return + + ct = response.headers.get('Content-Type', '').split(';')[0] + for coding in acceptable: + if coding.value == 'identity' and coding.qvalue != 0: + if debug: + cherrypy.log('Non-zero identity qvalue: %s' % coding, + context='TOOLS.GZIP') + return + if coding.value in ('gzip', 'x-gzip'): + if coding.qvalue == 0: + if debug: + cherrypy.log('Zero gzip qvalue: %s' % coding, + context='TOOLS.GZIP') + return + + if ct not in mime_types: + # If the list of provided mime-types contains tokens + # such as 'text/*' or 'application/*+xml', + # we go through them and find the most appropriate one + # based on the given content-type. + # The pattern matching is only caring about the most + # common cases, as stated above, and doesn't support + # for extra parameters. + found = False + if '/' in ct: + ct_media_type, ct_sub_type = ct.split('/') + for mime_type in mime_types: + if '/' in mime_type: + media_type, sub_type = mime_type.split('/') + if ct_media_type == media_type: + if sub_type == '*': + found = True + break + elif '+' in sub_type and '+' in ct_sub_type: + ct_left, ct_right = ct_sub_type.split('+') + left, right = sub_type.split('+') + if left == '*' and ct_right == right: + found = True + break + + if not found: + if debug: + cherrypy.log('Content-Type %s not in mime_types %r' % + (ct, mime_types), context='TOOLS.GZIP') + return + + if debug: + cherrypy.log('Gzipping', context='TOOLS.GZIP') + # Return a generator that compresses the page + response.headers['Content-Encoding'] = 'gzip' + response.body = compress(response.body, compress_level) + if 'Content-Length' in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers['Content-Length'] + + return + + if debug: + cherrypy.log('No acceptable encoding found.', context='GZIP') + cherrypy.HTTPError(406, 'identity, gzip').set_response() diff --git a/resources/lib/cherrypy/lib/gctools.py b/resources/lib/cherrypy/lib/gctools.py new file mode 100644 index 0000000..26746d7 --- /dev/null +++ b/resources/lib/cherrypy/lib/gctools.py @@ -0,0 +1,218 @@ +import gc +import inspect +import sys +import time + +try: + import objgraph +except ImportError: + objgraph = None + +import cherrypy +from cherrypy import _cprequest, _cpwsgi +from cherrypy.process.plugins import SimplePlugin + + +class ReferrerTree(object): + + """An object which gathers all referrers of an object to a given depth.""" + + peek_length = 40 + + def __init__(self, ignore=None, maxdepth=2, maxparents=10): + self.ignore = ignore or [] + self.ignore.append(inspect.currentframe().f_back) + self.maxdepth = maxdepth + self.maxparents = maxparents + + def ascend(self, obj, depth=1): + """Return a nested list containing referrers of the given object.""" + depth += 1 + parents = [] + + # Gather all referrers in one step to minimize + # cascading references due to repr() logic. + refs = gc.get_referrers(obj) + self.ignore.append(refs) + if len(refs) > self.maxparents: + return [('[%s referrers]' % len(refs), [])] + + try: + ascendcode = self.ascend.__code__ + except AttributeError: + ascendcode = self.ascend.im_func.func_code + for parent in refs: + if inspect.isframe(parent) and parent.f_code is ascendcode: + continue + if parent in self.ignore: + continue + if depth <= self.maxdepth: + parents.append((parent, self.ascend(parent, depth))) + else: + parents.append((parent, [])) + + return parents + + def peek(self, s): + """Return s, restricted to a sane length.""" + if len(s) > (self.peek_length + 3): + half = self.peek_length // 2 + return s[:half] + '...' + s[-half:] + else: + return s + + def _format(self, obj, descend=True): + """Return a string representation of a single object.""" + if inspect.isframe(obj): + filename, lineno, func, context, index = inspect.getframeinfo(obj) + return "" % func + + if not descend: + return self.peek(repr(obj)) + + if isinstance(obj, dict): + return '{' + ', '.join(['%s: %s' % (self._format(k, descend=False), + self._format(v, descend=False)) + for k, v in obj.items()]) + '}' + elif isinstance(obj, list): + return '[' + ', '.join([self._format(item, descend=False) + for item in obj]) + ']' + elif isinstance(obj, tuple): + return '(' + ', '.join([self._format(item, descend=False) + for item in obj]) + ')' + + r = self.peek(repr(obj)) + if isinstance(obj, (str, int, float)): + return r + return '%s: %s' % (type(obj), r) + + def format(self, tree): + """Return a list of string reprs from a nested list of referrers.""" + output = [] + + def ascend(branch, depth=1): + for parent, grandparents in branch: + output.append((' ' * depth) + self._format(parent)) + if grandparents: + ascend(grandparents, depth + 1) + ascend(tree) + return output + + +def get_instances(cls): + return [x for x in gc.get_objects() if isinstance(x, cls)] + + +class RequestCounter(SimplePlugin): + + def start(self): + self.count = 0 + + def before_request(self): + self.count += 1 + + def after_request(self): + self.count -= 1 + + +request_counter = RequestCounter(cherrypy.engine) +request_counter.subscribe() + + +def get_context(obj): + if isinstance(obj, _cprequest.Request): + return 'path=%s;stage=%s' % (obj.path_info, obj.stage) + elif isinstance(obj, _cprequest.Response): + return 'status=%s' % obj.status + elif isinstance(obj, _cpwsgi.AppResponse): + return 'PATH_INFO=%s' % obj.environ.get('PATH_INFO', '') + elif hasattr(obj, 'tb_lineno'): + return 'tb_lineno=%s' % obj.tb_lineno + return '' + + +class GCRoot(object): + + """A CherryPy page handler for testing reference leaks.""" + + classes = [ + (_cprequest.Request, 2, 2, + 'Should be 1 in this request thread and 1 in the main thread.'), + (_cprequest.Response, 2, 2, + 'Should be 1 in this request thread and 1 in the main thread.'), + (_cpwsgi.AppResponse, 1, 1, + 'Should be 1 in this request thread only.'), + ] + + @cherrypy.expose + def index(self): + return 'Hello, world!' + + @cherrypy.expose + def stats(self): + output = ['Statistics:'] + + for trial in range(10): + if request_counter.count > 0: + break + time.sleep(0.5) + else: + output.append('\nNot all requests closed properly.') + + # gc_collect isn't perfectly synchronous, because it may + # break reference cycles that then take time to fully + # finalize. Call it thrice and hope for the best. + gc.collect() + gc.collect() + unreachable = gc.collect() + if unreachable: + if objgraph is not None: + final = objgraph.by_type('Nondestructible') + if final: + objgraph.show_backrefs(final, filename='finalizers.png') + + trash = {} + for x in gc.garbage: + trash[type(x)] = trash.get(type(x), 0) + 1 + if trash: + output.insert(0, '\n%s unreachable objects:' % unreachable) + trash = [(v, k) for k, v in trash.items()] + trash.sort() + for pair in trash: + output.append(' ' + repr(pair)) + + # Check declared classes to verify uncollected instances. + # These don't have to be part of a cycle; they can be + # any objects that have unanticipated referrers that keep + # them from being collected. + allobjs = {} + for cls, minobj, maxobj, msg in self.classes: + allobjs[cls] = get_instances(cls) + + for cls, minobj, maxobj, msg in self.classes: + objs = allobjs[cls] + lenobj = len(objs) + if lenobj < minobj or lenobj > maxobj: + if minobj == maxobj: + output.append( + '\nExpected %s %r references, got %s.' % + (minobj, cls, lenobj)) + else: + output.append( + '\nExpected %s to %s %r references, got %s.' % + (minobj, maxobj, cls, lenobj)) + + for obj in objs: + if objgraph is not None: + ig = [id(objs), id(inspect.currentframe())] + fname = 'graph_%s_%s.png' % (cls.__name__, id(obj)) + objgraph.show_backrefs( + obj, extra_ignore=ig, max_depth=4, too_many=20, + filename=fname, extra_info=get_context) + output.append('\nReferrers for %s (refcount=%s):' % + (repr(obj), sys.getrefcount(obj))) + t = ReferrerTree(ignore=[objs], maxdepth=3) + tree = t.ascend(obj) + output.extend(t.format(tree)) + + return '\n'.join(output) diff --git a/resources/lib/cherrypy/lib/httputil.py b/resources/lib/cherrypy/lib/httputil.py new file mode 100644 index 0000000..59bcc74 --- /dev/null +++ b/resources/lib/cherrypy/lib/httputil.py @@ -0,0 +1,582 @@ +"""HTTP library functions. + +This module contains functions for building an HTTP application +framework: any one, not just one whose name starts with "Ch". ;) If you +reference any modules from some popular framework inside *this* module, +FuManChu will personally hang you up by your thumbs and submit you +to a public caning. +""" + +import functools +import email.utils +import re +from binascii import b2a_base64 +from cgi import parse_header +from email.header import decode_header + +import six +from six.moves import range, builtins, map +from six.moves.BaseHTTPServer import BaseHTTPRequestHandler + +import cherrypy +from cherrypy._cpcompat import ntob, ntou +from cherrypy._cpcompat import unquote_plus + +response_codes = BaseHTTPRequestHandler.responses.copy() + +# From https://github.com/cherrypy/cherrypy/issues/361 +response_codes[500] = ('Internal Server Error', + 'The server encountered an unexpected condition ' + 'which prevented it from fulfilling the request.') +response_codes[503] = ('Service Unavailable', + 'The server is currently unable to handle the ' + 'request due to a temporary overloading or ' + 'maintenance of the server.') + + +HTTPDate = functools.partial(email.utils.formatdate, usegmt=True) + + +def urljoin(*atoms): + r"""Return the given path \*atoms, joined into a single URL. + + This will correctly join a SCRIPT_NAME and PATH_INFO into the + original URL, even if either atom is blank. + """ + url = '/'.join([x for x in atoms if x]) + while '//' in url: + url = url.replace('//', '/') + # Special-case the final url of "", and return "/" instead. + return url or '/' + + +def urljoin_bytes(*atoms): + """Return the given path `*atoms`, joined into a single URL. + + This will correctly join a SCRIPT_NAME and PATH_INFO into the + original URL, even if either atom is blank. + """ + url = b'/'.join([x for x in atoms if x]) + while b'//' in url: + url = url.replace(b'//', b'/') + # Special-case the final url of "", and return "/" instead. + return url or b'/' + + +def protocol_from_http(protocol_str): + """Return a protocol tuple from the given 'HTTP/x.y' string.""" + return int(protocol_str[5]), int(protocol_str[7]) + + +def get_ranges(headervalue, content_length): + """Return a list of (start, stop) indices from a Range header, or None. + + Each (start, stop) tuple will be composed of two ints, which are suitable + for use in a slicing operation. That is, the header "Range: bytes=3-6", + if applied against a Python string, is requesting resource[3:7]. This + function will return the list [(3, 7)]. + + If this function returns an empty list, you should return HTTP 416. + """ + + if not headervalue: + return None + + result = [] + bytesunit, byteranges = headervalue.split('=', 1) + for brange in byteranges.split(','): + start, stop = [x.strip() for x in brange.split('-', 1)] + if start: + if not stop: + stop = content_length - 1 + start, stop = int(start), int(stop) + if start >= content_length: + # From rfc 2616 sec 14.16: + # "If the server receives a request (other than one + # including an If-Range request-header field) with an + # unsatisfiable Range request-header field (that is, + # all of whose byte-range-spec values have a first-byte-pos + # value greater than the current length of the selected + # resource), it SHOULD return a response code of 416 + # (Requested range not satisfiable)." + continue + if stop < start: + # From rfc 2616 sec 14.16: + # "If the server ignores a byte-range-spec because it + # is syntactically invalid, the server SHOULD treat + # the request as if the invalid Range header field + # did not exist. (Normally, this means return a 200 + # response containing the full entity)." + return None + result.append((start, stop + 1)) + else: + if not stop: + # See rfc quote above. + return None + # Negative subscript (last N bytes) + # + # RFC 2616 Section 14.35.1: + # If the entity is shorter than the specified suffix-length, + # the entire entity-body is used. + if int(stop) > content_length: + result.append((0, content_length)) + else: + result.append((content_length - int(stop), content_length)) + + return result + + +class HeaderElement(object): + + """An element (with parameters) from an HTTP header's element list.""" + + def __init__(self, value, params=None): + self.value = value + if params is None: + params = {} + self.params = params + + def __cmp__(self, other): + return builtins.cmp(self.value, other.value) + + def __lt__(self, other): + return self.value < other.value + + def __str__(self): + p = [';%s=%s' % (k, v) for k, v in six.iteritems(self.params)] + return str('%s%s' % (self.value, ''.join(p))) + + def __bytes__(self): + return ntob(self.__str__()) + + def __unicode__(self): + return ntou(self.__str__()) + + @staticmethod + def parse(elementstr): + """Transform 'token;key=val' to ('token', {'key': 'val'}).""" + initial_value, params = parse_header(elementstr) + return initial_value, params + + @classmethod + def from_str(cls, elementstr): + """Construct an instance from a string of the form 'token;key=val'.""" + ival, params = cls.parse(elementstr) + return cls(ival, params) + + +q_separator = re.compile(r'; *q *=') + + +class AcceptElement(HeaderElement): + + """An element (with parameters) from an Accept* header's element list. + + AcceptElement objects are comparable; the more-preferred object will be + "less than" the less-preferred object. They are also therefore sortable; + if you sort a list of AcceptElement objects, they will be listed in + priority order; the most preferred value will be first. Yes, it should + have been the other way around, but it's too late to fix now. + """ + + @classmethod + def from_str(cls, elementstr): + qvalue = None + # The first "q" parameter (if any) separates the initial + # media-range parameter(s) (if any) from the accept-params. + atoms = q_separator.split(elementstr, 1) + media_range = atoms.pop(0).strip() + if atoms: + # The qvalue for an Accept header can have extensions. The other + # headers cannot, but it's easier to parse them as if they did. + qvalue = HeaderElement.from_str(atoms[0].strip()) + + media_type, params = cls.parse(media_range) + if qvalue is not None: + params['q'] = qvalue + return cls(media_type, params) + + @property + def qvalue(self): + 'The qvalue, or priority, of this value.' + val = self.params.get('q', '1') + if isinstance(val, HeaderElement): + val = val.value + try: + return float(val) + except ValueError as val_err: + """Fail client requests with invalid quality value. + + Ref: https://github.com/cherrypy/cherrypy/issues/1370 + """ + six.raise_from( + cherrypy.HTTPError( + 400, + 'Malformed HTTP header: `{}`'. + format(str(self)), + ), + val_err, + ) + + def __cmp__(self, other): + diff = builtins.cmp(self.qvalue, other.qvalue) + if diff == 0: + diff = builtins.cmp(str(self), str(other)) + return diff + + def __lt__(self, other): + if self.qvalue == other.qvalue: + return str(self) < str(other) + else: + return self.qvalue < other.qvalue + + +RE_HEADER_SPLIT = re.compile(',(?=(?:[^"]*"[^"]*")*[^"]*$)') + + +def header_elements(fieldname, fieldvalue): + """Return a sorted HeaderElement list from a comma-separated header string. + """ + if not fieldvalue: + return [] + + result = [] + for element in RE_HEADER_SPLIT.split(fieldvalue): + if fieldname.startswith('Accept') or fieldname == 'TE': + hv = AcceptElement.from_str(element) + else: + hv = HeaderElement.from_str(element) + result.append(hv) + + return list(reversed(sorted(result))) + + +def decode_TEXT(value): + r""" + Decode :rfc:`2047` TEXT + + >>> decode_TEXT("=?utf-8?q?f=C3=BCr?=") == b'f\xfcr'.decode('latin-1') + True + """ + atoms = decode_header(value) + decodedvalue = '' + for atom, charset in atoms: + if charset is not None: + atom = atom.decode(charset) + decodedvalue += atom + return decodedvalue + + +def decode_TEXT_maybe(value): + """ + Decode the text but only if '=?' appears in it. + """ + return decode_TEXT(value) if '=?' in value else value + + +def valid_status(status): + """Return legal HTTP status Code, Reason-phrase and Message. + + The status arg must be an int, a str that begins with an int + or the constant from ``http.client`` stdlib module. + + If status has no reason-phrase is supplied, a default reason- + phrase will be provided. + + >>> from six.moves import http_client + >>> from six.moves.BaseHTTPServer import BaseHTTPRequestHandler + >>> valid_status(http_client.ACCEPTED) == ( + ... int(http_client.ACCEPTED), + ... ) + BaseHTTPRequestHandler.responses[http_client.ACCEPTED] + True + """ + + if not status: + status = 200 + + code, reason = status, None + if isinstance(status, six.string_types): + code, _, reason = status.partition(' ') + reason = reason.strip() or None + + try: + code = int(code) + except (TypeError, ValueError): + raise ValueError('Illegal response status from server ' + '(%s is non-numeric).' % repr(code)) + + if code < 100 or code > 599: + raise ValueError('Illegal response status from server ' + '(%s is out of range).' % repr(code)) + + if code not in response_codes: + # code is unknown but not illegal + default_reason, message = '', '' + else: + default_reason, message = response_codes[code] + + if reason is None: + reason = default_reason + + return code, reason, message + + +# NOTE: the parse_qs functions that follow are modified version of those +# in the python3.0 source - we need to pass through an encoding to the unquote +# method, but the default parse_qs function doesn't allow us to. These do. + +def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'): + """Parse a query given as a string argument. + + Arguments: + + qs: URL-encoded query string to be parsed + + keep_blank_values: flag indicating whether blank values in + URL encoded queries should be treated as blank strings. A + true value indicates that blanks should be retained as blank + strings. The default false value indicates that blank values + are to be ignored and treated as if they were not included. + + strict_parsing: flag indicating what to do with parsing errors. If + false (the default), errors are silently ignored. If true, + errors raise a ValueError exception. + + Returns a dict, as G-d intended. + """ + pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')] + d = {} + for name_value in pairs: + if not name_value and not strict_parsing: + continue + nv = name_value.split('=', 1) + if len(nv) != 2: + if strict_parsing: + raise ValueError('bad query field: %r' % (name_value,)) + # Handle case of a control-name with no equal sign + if keep_blank_values: + nv.append('') + else: + continue + if len(nv[1]) or keep_blank_values: + name = unquote_plus(nv[0], encoding, errors='strict') + value = unquote_plus(nv[1], encoding, errors='strict') + if name in d: + if not isinstance(d[name], list): + d[name] = [d[name]] + d[name].append(value) + else: + d[name] = value + return d + + +image_map_pattern = re.compile(r'[0-9]+,[0-9]+') + + +def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'): + """Build a params dictionary from a query_string. + + Duplicate key/value pairs in the provided query_string will be + returned as {'key': [val1, val2, ...]}. Single key/values will + be returned as strings: {'key': 'value'}. + """ + if image_map_pattern.match(query_string): + # Server-side image map. Map the coords to 'x' and 'y' + # (like CGI::Request does). + pm = query_string.split(',') + pm = {'x': int(pm[0]), 'y': int(pm[1])} + else: + pm = _parse_qs(query_string, keep_blank_values, encoding=encoding) + return pm + + +#### +# Inlined from jaraco.collections 1.5.2 +# Ref #1673 +class KeyTransformingDict(dict): + """ + A dict subclass that transforms the keys before they're used. + Subclasses may override the default transform_key to customize behavior. + """ + @staticmethod + def transform_key(key): + return key + + def __init__(self, *args, **kargs): + super(KeyTransformingDict, self).__init__() + # build a dictionary using the default constructs + d = dict(*args, **kargs) + # build this dictionary using transformed keys. + for item in d.items(): + self.__setitem__(*item) + + def __setitem__(self, key, val): + key = self.transform_key(key) + super(KeyTransformingDict, self).__setitem__(key, val) + + def __getitem__(self, key): + key = self.transform_key(key) + return super(KeyTransformingDict, self).__getitem__(key) + + def __contains__(self, key): + key = self.transform_key(key) + return super(KeyTransformingDict, self).__contains__(key) + + def __delitem__(self, key): + key = self.transform_key(key) + return super(KeyTransformingDict, self).__delitem__(key) + + def get(self, key, *args, **kwargs): + key = self.transform_key(key) + return super(KeyTransformingDict, self).get(key, *args, **kwargs) + + def setdefault(self, key, *args, **kwargs): + key = self.transform_key(key) + return super(KeyTransformingDict, self).setdefault( + key, *args, **kwargs) + + def pop(self, key, *args, **kwargs): + key = self.transform_key(key) + return super(KeyTransformingDict, self).pop(key, *args, **kwargs) + + def matching_key_for(self, key): + """ + Given a key, return the actual key stored in self that matches. + Raise KeyError if the key isn't found. + """ + try: + return next(e_key for e_key in self.keys() if e_key == key) + except StopIteration: + raise KeyError(key) +#### + + +class CaseInsensitiveDict(KeyTransformingDict): + + """A case-insensitive dict subclass. + + Each key is changed on entry to str(key).title(). + """ + + @staticmethod + def transform_key(key): + return str(key).title() + + +# TEXT = +# +# A CRLF is allowed in the definition of TEXT only as part of a header +# field continuation. It is expected that the folding LWS will be +# replaced with a single SP before interpretation of the TEXT value." +if str == bytes: + header_translate_table = ''.join([chr(i) for i in range(256)]) + header_translate_deletechars = ''.join( + [chr(i) for i in range(32)]) + chr(127) +else: + header_translate_table = None + header_translate_deletechars = bytes(range(32)) + bytes([127]) + + +class HeaderMap(CaseInsensitiveDict): + + """A dict subclass for HTTP request and response headers. + + Each key is changed on entry to str(key).title(). This allows headers + to be case-insensitive and avoid duplicates. + + Values are header values (decoded according to :rfc:`2047` if necessary). + """ + + protocol = (1, 1) + encodings = ['ISO-8859-1'] + + # Someday, when http-bis is done, this will probably get dropped + # since few servers, clients, or intermediaries do it. But until then, + # we're going to obey the spec as is. + # "Words of *TEXT MAY contain characters from character sets other than + # ISO-8859-1 only when encoded according to the rules of RFC 2047." + use_rfc_2047 = True + + def elements(self, key): + """Return a sorted list of HeaderElements for the given header.""" + key = str(key).title() + value = self.get(key) + return header_elements(key, value) + + def values(self, key): + """Return a sorted list of HeaderElement.value for the given header.""" + return [e.value for e in self.elements(key)] + + def output(self): + """Transform self into a list of (name, value) tuples.""" + return list(self.encode_header_items(self.items())) + + @classmethod + def encode_header_items(cls, header_items): + """ + Prepare the sequence of name, value tuples into a form suitable for + transmitting on the wire for HTTP. + """ + for k, v in header_items: + if not isinstance(v, six.string_types) and \ + not isinstance(v, six.binary_type): + v = six.text_type(v) + + yield tuple(map(cls.encode_header_item, (k, v))) + + @classmethod + def encode_header_item(cls, item): + if isinstance(item, six.text_type): + item = cls.encode(item) + + # See header_translate_* constants above. + # Replace only if you really know what you're doing. + return item.translate( + header_translate_table, header_translate_deletechars) + + @classmethod + def encode(cls, v): + """Return the given header name or value, encoded for HTTP output.""" + for enc in cls.encodings: + try: + return v.encode(enc) + except UnicodeEncodeError: + continue + + if cls.protocol == (1, 1) and cls.use_rfc_2047: + # Encode RFC-2047 TEXT + # (e.g. u"\u8200" -> "=?utf-8?b?6IiA?="). + # We do our own here instead of using the email module + # because we never want to fold lines--folding has + # been deprecated by the HTTP working group. + v = b2a_base64(v.encode('utf-8')) + return (b'=?utf-8?b?' + v.strip(b'\n') + b'?=') + + raise ValueError('Could not encode header part %r using ' + 'any of the encodings %r.' % + (v, cls.encodings)) + + +class Host(object): + + """An internet address. + + name + Should be the client's host name. If not available (because no DNS + lookup is performed), the IP address should be used instead. + + """ + + ip = '0.0.0.0' + port = 80 + name = 'unknown.tld' + + def __init__(self, ip, port, name=None): + self.ip = ip + self.port = port + if name is None: + name = ip + self.name = name + + def __repr__(self): + return 'httputil.Host(%r, %r, %r)' % (self.ip, self.port, self.name) diff --git a/resources/lib/cherrypy/lib/jsontools.py b/resources/lib/cherrypy/lib/jsontools.py new file mode 100644 index 0000000..4868309 --- /dev/null +++ b/resources/lib/cherrypy/lib/jsontools.py @@ -0,0 +1,88 @@ +import cherrypy +from cherrypy._cpcompat import text_or_bytes, ntou, json_encode, json_decode + + +def json_processor(entity): + """Read application/json data into request.json.""" + if not entity.headers.get(ntou('Content-Length'), ntou('')): + raise cherrypy.HTTPError(411) + + body = entity.fp.read() + with cherrypy.HTTPError.handle(ValueError, 400, 'Invalid JSON document'): + cherrypy.serving.request.json = json_decode(body.decode('utf-8')) + + +def json_in(content_type=[ntou('application/json'), ntou('text/javascript')], + force=True, debug=False, processor=json_processor): + """Add a processor to parse JSON request entities: + The default processor places the parsed data into request.json. + + Incoming request entities which match the given content_type(s) will + be deserialized from JSON to the Python equivalent, and the result + stored at cherrypy.request.json. The 'content_type' argument may + be a Content-Type string or a list of allowable Content-Type strings. + + If the 'force' argument is True (the default), then entities of other + content types will not be allowed; "415 Unsupported Media Type" is + raised instead. + + Supply your own processor to use a custom decoder, or to handle the parsed + data differently. The processor can be configured via + tools.json_in.processor or via the decorator method. + + Note that the deserializer requires the client send a Content-Length + request header, or it will raise "411 Length Required". If for any + other reason the request entity cannot be deserialized from JSON, + it will raise "400 Bad Request: Invalid JSON document". + """ + request = cherrypy.serving.request + if isinstance(content_type, text_or_bytes): + content_type = [content_type] + + if force: + if debug: + cherrypy.log('Removing body processors %s' % + repr(request.body.processors.keys()), 'TOOLS.JSON_IN') + request.body.processors.clear() + request.body.default_proc = cherrypy.HTTPError( + 415, 'Expected an entity of content type %s' % + ', '.join(content_type)) + + for ct in content_type: + if debug: + cherrypy.log('Adding body processor for %s' % ct, 'TOOLS.JSON_IN') + request.body.processors[ct] = processor + + +def json_handler(*args, **kwargs): + value = cherrypy.serving.request._json_inner_handler(*args, **kwargs) + return json_encode(value) + + +def json_out(content_type='application/json', debug=False, + handler=json_handler): + """Wrap request.handler to serialize its output to JSON. Sets Content-Type. + + If the given content_type is None, the Content-Type response header + is not set. + + Provide your own handler to use a custom encoder. For example + cherrypy.config['tools.json_out.handler'] = , or + @json_out(handler=function). + """ + request = cherrypy.serving.request + # request.handler may be set to None by e.g. the caching tool + # to signal to all components that a response body has already + # been attached, in which case we don't need to wrap anything. + if request.handler is None: + return + if debug: + cherrypy.log('Replacing %s with JSON handler' % request.handler, + 'TOOLS.JSON_OUT') + request._json_inner_handler = request.handler + request.handler = handler + if content_type is not None: + if debug: + cherrypy.log('Setting Content-Type to %s' % + content_type, 'TOOLS.JSON_OUT') + cherrypy.serving.response.headers['Content-Type'] = content_type diff --git a/resources/lib/cherrypy/lib/locking.py b/resources/lib/cherrypy/lib/locking.py new file mode 100644 index 0000000..317fb58 --- /dev/null +++ b/resources/lib/cherrypy/lib/locking.py @@ -0,0 +1,47 @@ +import datetime + + +class NeverExpires(object): + def expired(self): + return False + + +class Timer(object): + """ + A simple timer that will indicate when an expiration time has passed. + """ + def __init__(self, expiration): + 'Create a timer that expires at `expiration` (UTC datetime)' + self.expiration = expiration + + @classmethod + def after(cls, elapsed): + """ + Return a timer that will expire after `elapsed` passes. + """ + return cls(datetime.datetime.utcnow() + elapsed) + + def expired(self): + return datetime.datetime.utcnow() >= self.expiration + + +class LockTimeout(Exception): + 'An exception when a lock could not be acquired before a timeout period' + + +class LockChecker(object): + """ + Keep track of the time and detect if a timeout has expired + """ + def __init__(self, session_id, timeout): + self.session_id = session_id + if timeout: + self.timer = Timer.after(timeout) + else: + self.timer = NeverExpires() + + def expired(self): + if self.timer.expired(): + raise LockTimeout( + 'Timeout acquiring lock for %(session_id)s' % vars(self)) + return False diff --git a/resources/lib/cherrypy/lib/profiler.py b/resources/lib/cherrypy/lib/profiler.py new file mode 100644 index 0000000..fccf2eb --- /dev/null +++ b/resources/lib/cherrypy/lib/profiler.py @@ -0,0 +1,221 @@ +"""Profiler tools for CherryPy. + +CherryPy users +============== + +You can profile any of your pages as follows:: + + from cherrypy.lib import profiler + + class Root: + p = profiler.Profiler("/path/to/profile/dir") + + @cherrypy.expose + def index(self): + self.p.run(self._index) + + def _index(self): + return "Hello, world!" + + cherrypy.tree.mount(Root()) + +You can also turn on profiling for all requests +using the ``make_app`` function as WSGI middleware. + +CherryPy developers +=================== + +This module can be used whenever you make changes to CherryPy, +to get a quick sanity-check on overall CP performance. Use the +``--profile`` flag when running the test suite. Then, use the ``serve()`` +function to browse the results in a web browser. If you run this +module from the command line, it will call ``serve()`` for you. + +""" + +import io +import os +import os.path +import sys +import warnings + +import cherrypy + + +try: + import profile + import pstats + + def new_func_strip_path(func_name): + """Make profiler output more readable by adding `__init__` modules' parents + """ + filename, line, name = func_name + if filename.endswith('__init__.py'): + return ( + os.path.basename(filename[:-12]) + filename[-12:], + line, + name, + ) + return os.path.basename(filename), line, name + + pstats.func_strip_path = new_func_strip_path +except ImportError: + profile = None + pstats = None + + +_count = 0 + + +class Profiler(object): + + def __init__(self, path=None): + if not path: + path = os.path.join(os.path.dirname(__file__), 'profile') + self.path = path + if not os.path.exists(path): + os.makedirs(path) + + def run(self, func, *args, **params): + """Dump profile data into self.path.""" + global _count + c = _count = _count + 1 + path = os.path.join(self.path, 'cp_%04d.prof' % c) + prof = profile.Profile() + result = prof.runcall(func, *args, **params) + prof.dump_stats(path) + return result + + def statfiles(self): + """:rtype: list of available profiles. + """ + return [f for f in os.listdir(self.path) + if f.startswith('cp_') and f.endswith('.prof')] + + def stats(self, filename, sortby='cumulative'): + """:rtype stats(index): output of print_stats() for the given profile. + """ + sio = io.StringIO() + if sys.version_info >= (2, 5): + s = pstats.Stats(os.path.join(self.path, filename), stream=sio) + s.strip_dirs() + s.sort_stats(sortby) + s.print_stats() + else: + # pstats.Stats before Python 2.5 didn't take a 'stream' arg, + # but just printed to stdout. So re-route stdout. + s = pstats.Stats(os.path.join(self.path, filename)) + s.strip_dirs() + s.sort_stats(sortby) + oldout = sys.stdout + try: + sys.stdout = sio + s.print_stats() + finally: + sys.stdout = oldout + response = sio.getvalue() + sio.close() + return response + + @cherrypy.expose + def index(self): + return """ + CherryPy profile data + + + + + + """ + + @cherrypy.expose + def menu(self): + yield '

Profiling runs

' + yield '

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

' + runs = self.statfiles() + runs.sort() + for i in runs: + yield "%s
" % ( + i, i) + + @cherrypy.expose + def report(self, filename): + cherrypy.response.headers['Content-Type'] = 'text/plain' + return self.stats(filename) + + +class ProfileAggregator(Profiler): + + def __init__(self, path=None): + Profiler.__init__(self, path) + global _count + self.count = _count = _count + 1 + self.profiler = profile.Profile() + + def run(self, func, *args, **params): + path = os.path.join(self.path, 'cp_%04d.prof' % self.count) + result = self.profiler.runcall(func, *args, **params) + self.profiler.dump_stats(path) + return result + + +class make_app: + + def __init__(self, nextapp, path=None, aggregate=False): + """Make a WSGI middleware app which wraps 'nextapp' with profiling. + + nextapp + the WSGI application to wrap, usually an instance of + cherrypy.Application. + + path + where to dump the profiling output. + + aggregate + if True, profile data for all HTTP requests will go in + a single file. If False (the default), each HTTP request will + dump its profile data into a separate file. + + """ + if profile is None or pstats is None: + msg = ('Your installation of Python does not have a profile ' + "module. If you're on Debian, try " + '`sudo apt-get install python-profiler`. ' + 'See http://www.cherrypy.org/wiki/ProfilingOnDebian ' + 'for details.') + warnings.warn(msg) + + self.nextapp = nextapp + self.aggregate = aggregate + if aggregate: + self.profiler = ProfileAggregator(path) + else: + self.profiler = Profiler(path) + + def __call__(self, environ, start_response): + def gather(): + result = [] + for line in self.nextapp(environ, start_response): + result.append(line) + return result + return self.profiler.run(gather) + + +def serve(path=None, port=8080): + if profile is None or pstats is None: + msg = ('Your installation of Python does not have a profile module. ' + "If you're on Debian, try " + '`sudo apt-get install python-profiler`. ' + 'See http://www.cherrypy.org/wiki/ProfilingOnDebian ' + 'for details.') + warnings.warn(msg) + + cherrypy.config.update({'server.socket_port': int(port), + 'server.thread_pool': 10, + 'environment': 'production', + }) + cherrypy.quickstart(Profiler(path)) + + +if __name__ == '__main__': + serve(*tuple(sys.argv[1:])) diff --git a/resources/lib/cherrypy/lib/reprconf.py b/resources/lib/cherrypy/lib/reprconf.py new file mode 100644 index 0000000..fc75849 --- /dev/null +++ b/resources/lib/cherrypy/lib/reprconf.py @@ -0,0 +1,516 @@ +"""Generic configuration system using unrepr. + +Configuration data may be supplied as a Python dictionary, as a filename, +or as an open file object. When you supply a filename or file, Python's +builtin ConfigParser is used (with some extensions). + +Namespaces +---------- + +Configuration keys are separated into namespaces by the first "." in the key. + +The only key that cannot exist in a namespace is the "environment" entry. +This special entry 'imports' other config entries from a template stored in +the Config.environments dict. + +You can define your own namespaces to be called when new config is merged +by adding a named handler to Config.namespaces. The name can be any string, +and the handler must be either a callable or a context manager. +""" + +from cherrypy._cpcompat import text_or_bytes +from six.moves import configparser +from six.moves import builtins + +import operator +import sys + + +class NamespaceSet(dict): + + """A dict of config namespace names and handlers. + + Each config entry should begin with a namespace name; the corresponding + namespace handler will be called once for each config entry in that + namespace, and will be passed two arguments: the config key (with the + namespace removed) and the config value. + + Namespace handlers may be any Python callable; they may also be + Python 2.5-style 'context managers', in which case their __enter__ + method should return a callable to be used as the handler. + See cherrypy.tools (the Toolbox class) for an example. + """ + + def __call__(self, config): + """Iterate through config and pass it to each namespace handler. + + config + A flat dict, where keys use dots to separate + namespaces, and values are arbitrary. + + The first name in each config key is used to look up the corresponding + namespace handler. For example, a config entry of {'tools.gzip.on': v} + will call the 'tools' namespace handler with the args: ('gzip.on', v) + """ + # Separate the given config into namespaces + ns_confs = {} + for k in config: + if '.' in k: + ns, name = k.split('.', 1) + bucket = ns_confs.setdefault(ns, {}) + bucket[name] = config[k] + + # I chose __enter__ and __exit__ so someday this could be + # rewritten using Python 2.5's 'with' statement: + # for ns, handler in six.iteritems(self): + # with handler as callable: + # for k, v in six.iteritems(ns_confs.get(ns, {})): + # callable(k, v) + for ns, handler in self.items(): + exit = getattr(handler, '__exit__', None) + if exit: + callable = handler.__enter__() + no_exc = True + try: + try: + for k, v in ns_confs.get(ns, {}).items(): + callable(k, v) + except Exception: + # The exceptional case is handled here + no_exc = False + if exit is None: + raise + if not exit(*sys.exc_info()): + raise + # The exception is swallowed if exit() returns true + finally: + # The normal and non-local-goto cases are handled here + if no_exc and exit: + exit(None, None, None) + else: + for k, v in ns_confs.get(ns, {}).items(): + handler(k, v) + + def __repr__(self): + return '%s.%s(%s)' % (self.__module__, self.__class__.__name__, + dict.__repr__(self)) + + def __copy__(self): + newobj = self.__class__() + newobj.update(self) + return newobj + copy = __copy__ + + +class Config(dict): + + """A dict-like set of configuration data, with defaults and namespaces. + + May take a file, filename, or dict. + """ + + defaults = {} + environments = {} + namespaces = NamespaceSet() + + def __init__(self, file=None, **kwargs): + self.reset() + if file is not None: + self.update(file) + if kwargs: + self.update(kwargs) + + def reset(self): + """Reset self to default values.""" + self.clear() + dict.update(self, self.defaults) + + def update(self, config): + """Update self from a dict, file, or filename.""" + self._apply(Parser.load(config)) + + def _apply(self, config): + """Update self from a dict.""" + which_env = config.get('environment') + if which_env: + env = self.environments[which_env] + for k in env: + if k not in config: + config[k] = env[k] + + dict.update(self, config) + self.namespaces(config) + + def __setitem__(self, k, v): + dict.__setitem__(self, k, v) + self.namespaces({k: v}) + + +class Parser(configparser.ConfigParser): + + """Sub-class of ConfigParser that keeps the case of options and that + raises an exception if the file cannot be read. + """ + + def optionxform(self, optionstr): + return optionstr + + def read(self, filenames): + if isinstance(filenames, text_or_bytes): + filenames = [filenames] + for filename in filenames: + # try: + # fp = open(filename) + # except IOError: + # continue + fp = open(filename) + try: + self._read(fp, filename) + finally: + fp.close() + + def as_dict(self, raw=False, vars=None): + """Convert an INI file to a dictionary""" + # Load INI file into a dict + result = {} + for section in self.sections(): + if section not in result: + result[section] = {} + for option in self.options(section): + value = self.get(section, option, raw=raw, vars=vars) + try: + value = unrepr(value) + except Exception: + x = sys.exc_info()[1] + msg = ('Config error in section: %r, option: %r, ' + 'value: %r. Config values must be valid Python.' % + (section, option, value)) + raise ValueError(msg, x.__class__.__name__, x.args) + result[section][option] = value + return result + + def dict_from_file(self, file): + if hasattr(file, 'read'): + self.readfp(file) + else: + self.read(file) + return self.as_dict() + + @classmethod + def load(self, input): + """Resolve 'input' to dict from a dict, file, or filename.""" + is_file = ( + # Filename + isinstance(input, text_or_bytes) + # Open file object + or hasattr(input, 'read') + ) + return Parser().dict_from_file(input) if is_file else input.copy() + + +# public domain "unrepr" implementation, found on the web and then improved. + + +class _Builder2: + + def build(self, o): + m = getattr(self, 'build_' + o.__class__.__name__, None) + if m is None: + raise TypeError('unrepr does not recognize %s' % + repr(o.__class__.__name__)) + return m(o) + + def astnode(self, s): + """Return a Python2 ast Node compiled from a string.""" + try: + import compiler + except ImportError: + # Fallback to eval when compiler package is not available, + # e.g. IronPython 1.0. + return eval(s) + + p = compiler.parse('__tempvalue__ = ' + s) + return p.getChildren()[1].getChildren()[0].getChildren()[1] + + def build_Subscript(self, o): + expr, flags, subs = o.getChildren() + expr = self.build(expr) + subs = self.build(subs) + return expr[subs] + + def build_CallFunc(self, o): + children = o.getChildren() + # Build callee from first child + callee = self.build(children[0]) + # Build args and kwargs from remaining children + args = [] + kwargs = {} + for child in children[1:]: + class_name = child.__class__.__name__ + # None is ignored + if class_name == 'NoneType': + continue + # Keywords become kwargs + if class_name == 'Keyword': + kwargs.update(self.build(child)) + # Everything else becomes args + else: + args.append(self.build(child)) + + return callee(*args, **kwargs) + + def build_Keyword(self, o): + key, value_obj = o.getChildren() + value = self.build(value_obj) + kw_dict = {key: value} + return kw_dict + + def build_List(self, o): + return map(self.build, o.getChildren()) + + def build_Const(self, o): + return o.value + + def build_Dict(self, o): + d = {} + i = iter(map(self.build, o.getChildren())) + for el in i: + d[el] = i.next() + return d + + def build_Tuple(self, o): + return tuple(self.build_List(o)) + + def build_Name(self, o): + name = o.name + if name == 'None': + return None + if name == 'True': + return True + if name == 'False': + return False + + # See if the Name is a package or module. If it is, import it. + try: + return modules(name) + except ImportError: + pass + + # See if the Name is in builtins. + try: + return getattr(builtins, name) + except AttributeError: + pass + + raise TypeError('unrepr could not resolve the name %s' % repr(name)) + + def build_Add(self, o): + left, right = map(self.build, o.getChildren()) + return left + right + + def build_Mul(self, o): + left, right = map(self.build, o.getChildren()) + return left * right + + def build_Getattr(self, o): + parent = self.build(o.expr) + return getattr(parent, o.attrname) + + def build_NoneType(self, o): + return None + + def build_UnarySub(self, o): + return -self.build(o.getChildren()[0]) + + def build_UnaryAdd(self, o): + return self.build(o.getChildren()[0]) + + +class _Builder3: + + def build(self, o): + m = getattr(self, 'build_' + o.__class__.__name__, None) + if m is None: + raise TypeError('unrepr does not recognize %s' % + repr(o.__class__.__name__)) + return m(o) + + def astnode(self, s): + """Return a Python3 ast Node compiled from a string.""" + try: + import ast + except ImportError: + # Fallback to eval when ast package is not available, + # e.g. IronPython 1.0. + return eval(s) + + p = ast.parse('__tempvalue__ = ' + s) + return p.body[0].value + + def build_Subscript(self, o): + return self.build(o.value)[self.build(o.slice)] + + def build_Index(self, o): + return self.build(o.value) + + def _build_call35(self, o): + """ + Workaround for python 3.5 _ast.Call signature, docs found here + https://greentreesnakes.readthedocs.org/en/latest/nodes.html + """ + import ast + callee = self.build(o.func) + args = [] + if o.args is not None: + for a in o.args: + if isinstance(a, ast.Starred): + args.append(self.build(a.value)) + else: + args.append(self.build(a)) + kwargs = {} + for kw in o.keywords: + if kw.arg is None: # double asterix `**` + rst = self.build(kw.value) + if not isinstance(rst, dict): + raise TypeError('Invalid argument for call.' + 'Must be a mapping object.') + # give preference to the keys set directly from arg=value + for k, v in rst.items(): + if k not in kwargs: + kwargs[k] = v + else: # defined on the call as: arg=value + kwargs[kw.arg] = self.build(kw.value) + return callee(*args, **kwargs) + + def build_Call(self, o): + if sys.version_info >= (3, 5): + return self._build_call35(o) + + callee = self.build(o.func) + + if o.args is None: + args = () + else: + args = tuple([self.build(a) for a in o.args]) + + if o.starargs is None: + starargs = () + else: + starargs = tuple(self.build(o.starargs)) + + if o.kwargs is None: + kwargs = {} + else: + kwargs = self.build(o.kwargs) + if o.keywords is not None: # direct a=b keywords + for kw in o.keywords: + # preference because is a direct keyword against **kwargs + kwargs[kw.arg] = self.build(kw.value) + return callee(*(args + starargs), **kwargs) + + def build_List(self, o): + return list(map(self.build, o.elts)) + + def build_Str(self, o): + return o.s + + def build_Num(self, o): + return o.n + + def build_Dict(self, o): + return dict([(self.build(k), self.build(v)) + for k, v in zip(o.keys, o.values)]) + + def build_Tuple(self, o): + return tuple(self.build_List(o)) + + def build_Name(self, o): + name = o.id + if name == 'None': + return None + if name == 'True': + return True + if name == 'False': + return False + + # See if the Name is a package or module. If it is, import it. + try: + return modules(name) + except ImportError: + pass + + # See if the Name is in builtins. + try: + import builtins + return getattr(builtins, name) + except AttributeError: + pass + + raise TypeError('unrepr could not resolve the name %s' % repr(name)) + + def build_NameConstant(self, o): + return o.value + + build_Constant = build_NameConstant # Python 3.8 change + + def build_UnaryOp(self, o): + op, operand = map(self.build, [o.op, o.operand]) + return op(operand) + + def build_BinOp(self, o): + left, op, right = map(self.build, [o.left, o.op, o.right]) + return op(left, right) + + def build_Add(self, o): + return operator.add + + def build_Mult(self, o): + return operator.mul + + def build_USub(self, o): + return operator.neg + + def build_Attribute(self, o): + parent = self.build(o.value) + return getattr(parent, o.attr) + + def build_NoneType(self, o): + return None + + +def unrepr(s): + """Return a Python object compiled from a string.""" + if not s: + return s + if sys.version_info < (3, 0): + b = _Builder2() + else: + b = _Builder3() + obj = b.astnode(s) + return b.build(obj) + + +def modules(modulePath): + """Load a module and retrieve a reference to that module.""" + __import__(modulePath) + return sys.modules[modulePath] + + +def attributes(full_attribute_name): + """Load a module and retrieve an attribute of that module.""" + + # Parse out the path, module, and attribute + last_dot = full_attribute_name.rfind('.') + attr_name = full_attribute_name[last_dot + 1:] + mod_path = full_attribute_name[:last_dot] + + mod = modules(mod_path) + # Let an AttributeError propagate outward. + try: + attr = getattr(mod, attr_name) + except AttributeError: + raise AttributeError("'%s' object has no attribute '%s'" + % (mod_path, attr_name)) + + # Return a reference to the attribute. + return attr diff --git a/resources/lib/cherrypy/lib/sessions.py b/resources/lib/cherrypy/lib/sessions.py new file mode 100644 index 0000000..5b49ee1 --- /dev/null +++ b/resources/lib/cherrypy/lib/sessions.py @@ -0,0 +1,919 @@ +"""Session implementation for CherryPy. + +You need to edit your config file to use sessions. Here's an example:: + + [/] + tools.sessions.on = True + tools.sessions.storage_class = cherrypy.lib.sessions.FileSession + tools.sessions.storage_path = "/home/site/sessions" + tools.sessions.timeout = 60 + +This sets the session to be stored in files in the directory +/home/site/sessions, and the session timeout to 60 minutes. If you omit +``storage_class``, the sessions will be saved in RAM. +``tools.sessions.on`` is the only required line for working sessions, +the rest are optional. + +By default, the session ID is passed in a cookie, so the client's browser must +have cookies enabled for your site. + +To set data for the current session, use +``cherrypy.session['fieldname'] = 'fieldvalue'``; +to get data use ``cherrypy.session.get('fieldname')``. + +================ +Locking sessions +================ + +By default, the ``'locking'`` mode of sessions is ``'implicit'``, which means +the session is locked early and unlocked late. Be mindful of this default mode +for any requests that take a long time to process (streaming responses, +expensive calculations, database lookups, API calls, etc), as other concurrent +requests that also utilize sessions will hang until the session is unlocked. + +If you want to control when the session data is locked and unlocked, +set ``tools.sessions.locking = 'explicit'``. Then call +``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_lock()``. +Regardless of which mode you use, the session is guaranteed to be unlocked when +the request is complete. + +================= +Expiring Sessions +================= + +You can force a session to expire with :func:`cherrypy.lib.sessions.expire`. +Simply call that function at the point you want the session to expire, and it +will cause the session cookie to expire client-side. + +=========================== +Session Fixation Protection +=========================== + +If CherryPy receives, via a request cookie, a session id that it does not +recognize, it will reject that id and create a new one to return in the +response cookie. This `helps prevent session fixation attacks +`_. +However, CherryPy "recognizes" a session id by looking up the saved session +data for that id. Therefore, if you never save any session data, +**you will get a new session id for every request**. + +A side effect of CherryPy overwriting unrecognised session ids is that if you +have multiple, separate CherryPy applications running on a single domain (e.g. +on different ports), each app will overwrite the other's session id because by +default they use the same cookie name (``"session_id"``) but do not recognise +each others sessions. It is therefore a good idea to use a different name for +each, for example:: + + [/] + ... + tools.sessions.name = "my_app_session_id" + +================ +Sharing Sessions +================ + +If you run multiple instances of CherryPy (for example via mod_python behind +Apache prefork), you most likely cannot use the RAM session backend, since each +instance of CherryPy will have its own memory space. Use a different backend +instead, and verify that all instances are pointing at the same file or db +location. Alternately, you might try a load balancer which makes sessions +"sticky". Google is your friend, there. + +================ +Expiration Dates +================ + +The response cookie will possess an expiration date to inform the client at +which point to stop sending the cookie back in requests. If the server time +and client time differ, expect sessions to be unreliable. **Make sure the +system time of your server is accurate**. + +CherryPy defaults to a 60-minute session timeout, which also applies to the +cookie which is sent to the client. Unfortunately, some versions of Safari +("4 public beta" on Windows XP at least) appear to have a bug in their parsing +of the GMT expiration date--they appear to interpret the date as one hour in +the past. Sixty minutes minus one hour is pretty close to zero, so you may +experience this bug as a new session id for every request, unless the requests +are less than one second apart. To fix, try increasing the session.timeout. + +On the other extreme, some users report Firefox sending cookies after their +expiration date, although this was on a system with an inaccurate system time. +Maybe FF doesn't trust system time. +""" +import sys +import datetime +import os +import time +import threading +import binascii + +import six +from six.moves import cPickle as pickle +import contextlib2 + +import zc.lockfile + +import cherrypy +from cherrypy.lib import httputil +from cherrypy.lib import locking +from cherrypy.lib import is_iterator + + +if six.PY2: + FileNotFoundError = OSError + + +missing = object() + + +class Session(object): + + """A CherryPy dict-like Session object (one per request).""" + + _id = None + + id_observers = None + "A list of callbacks to which to pass new id's." + + @property + def id(self): + """Return the current session id.""" + return self._id + + @id.setter + def id(self, value): + self._id = value + for o in self.id_observers: + o(value) + + timeout = 60 + 'Number of minutes after which to delete session data.' + + locked = False + """ + If True, this session instance has exclusive read/write access + to session data.""" + + loaded = False + """ + If True, data has been retrieved from storage. This should happen + automatically on the first attempt to access session data.""" + + clean_thread = None + 'Class-level Monitor which calls self.clean_up.' + + clean_freq = 5 + 'The poll rate for expired session cleanup in minutes.' + + originalid = None + 'The session id passed by the client. May be missing or unsafe.' + + missing = False + 'True if the session requested by the client did not exist.' + + regenerated = False + """ + True if the application called session.regenerate(). This is not set by + internal calls to regenerate the session id.""" + + debug = False + 'If True, log debug information.' + + # --------------------- Session management methods --------------------- # + + def __init__(self, id=None, **kwargs): + self.id_observers = [] + self._data = {} + + for k, v in kwargs.items(): + setattr(self, k, v) + + self.originalid = id + self.missing = False + if id is None: + if self.debug: + cherrypy.log('No id given; making a new one', 'TOOLS.SESSIONS') + self._regenerate() + else: + self.id = id + if self._exists(): + if self.debug: + cherrypy.log('Set id to %s.' % id, 'TOOLS.SESSIONS') + else: + if self.debug: + cherrypy.log('Expired or malicious session %r; ' + 'making a new one' % id, 'TOOLS.SESSIONS') + # Expired or malicious session. Make a new one. + # See https://github.com/cherrypy/cherrypy/issues/709. + self.id = None + self.missing = True + self._regenerate() + + def now(self): + """Generate the session specific concept of 'now'. + + Other session providers can override this to use alternative, + possibly timezone aware, versions of 'now'. + """ + return datetime.datetime.now() + + def regenerate(self): + """Replace the current session (with a new id).""" + self.regenerated = True + self._regenerate() + + def _regenerate(self): + if self.id is not None: + if self.debug: + cherrypy.log( + 'Deleting the existing session %r before ' + 'regeneration.' % self.id, + 'TOOLS.SESSIONS') + self.delete() + + old_session_was_locked = self.locked + if old_session_was_locked: + self.release_lock() + if self.debug: + cherrypy.log('Old lock released.', 'TOOLS.SESSIONS') + + self.id = None + while self.id is None: + self.id = self.generate_id() + # Assert that the generated id is not already stored. + if self._exists(): + self.id = None + if self.debug: + cherrypy.log('Set id to generated %s.' % self.id, + 'TOOLS.SESSIONS') + + if old_session_was_locked: + self.acquire_lock() + if self.debug: + cherrypy.log('Regenerated lock acquired.', 'TOOLS.SESSIONS') + + def clean_up(self): + """Clean up expired sessions.""" + pass + + def generate_id(self): + """Return a new session id.""" + return binascii.hexlify(os.urandom(20)).decode('ascii') + + def save(self): + """Save session data.""" + try: + # If session data has never been loaded then it's never been + # accessed: no need to save it + if self.loaded: + t = datetime.timedelta(seconds=self.timeout * 60) + expiration_time = self.now() + t + if self.debug: + cherrypy.log('Saving session %r with expiry %s' % + (self.id, expiration_time), + 'TOOLS.SESSIONS') + self._save(expiration_time) + else: + if self.debug: + cherrypy.log( + 'Skipping save of session %r (no session loaded).' % + self.id, 'TOOLS.SESSIONS') + finally: + if self.locked: + # Always release the lock if the user didn't release it + self.release_lock() + if self.debug: + cherrypy.log('Lock released after save.', 'TOOLS.SESSIONS') + + def load(self): + """Copy stored session data into this session instance.""" + data = self._load() + # data is either None or a tuple (session_data, expiration_time) + if data is None or data[1] < self.now(): + if self.debug: + cherrypy.log('Expired session %r, flushing data.' % self.id, + 'TOOLS.SESSIONS') + self._data = {} + else: + if self.debug: + cherrypy.log('Data loaded for session %r.' % self.id, + 'TOOLS.SESSIONS') + self._data = data[0] + self.loaded = True + + # Stick the clean_thread in the class, not the instance. + # The instances are created and destroyed per-request. + cls = self.__class__ + if self.clean_freq and not cls.clean_thread: + # clean_up is an instancemethod and not a classmethod, + # so that tool config can be accessed inside the method. + t = cherrypy.process.plugins.Monitor( + cherrypy.engine, self.clean_up, self.clean_freq * 60, + name='Session cleanup') + t.subscribe() + cls.clean_thread = t + t.start() + if self.debug: + cherrypy.log('Started cleanup thread.', 'TOOLS.SESSIONS') + + def delete(self): + """Delete stored session data.""" + self._delete() + if self.debug: + cherrypy.log('Deleted session %s.' % self.id, + 'TOOLS.SESSIONS') + + # -------------------- Application accessor methods -------------------- # + + def __getitem__(self, key): + if not self.loaded: + self.load() + return self._data[key] + + def __setitem__(self, key, value): + if not self.loaded: + self.load() + self._data[key] = value + + def __delitem__(self, key): + if not self.loaded: + self.load() + del self._data[key] + + def pop(self, key, default=missing): + """Remove the specified key and return the corresponding value. + If key is not found, default is returned if given, + otherwise KeyError is raised. + """ + if not self.loaded: + self.load() + if default is missing: + return self._data.pop(key) + else: + return self._data.pop(key, default) + + def __contains__(self, key): + if not self.loaded: + self.load() + return key in self._data + + def get(self, key, default=None): + """D.get(k[,d]) -> D[k] if k in D, else d. d defaults to None.""" + if not self.loaded: + self.load() + return self._data.get(key, default) + + def update(self, d): + """D.update(E) -> None. Update D from E: for k in E: D[k] = E[k].""" + if not self.loaded: + self.load() + self._data.update(d) + + def setdefault(self, key, default=None): + """D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D.""" + if not self.loaded: + self.load() + return self._data.setdefault(key, default) + + def clear(self): + """D.clear() -> None. Remove all items from D.""" + if not self.loaded: + self.load() + self._data.clear() + + def keys(self): + """D.keys() -> list of D's keys.""" + if not self.loaded: + self.load() + return self._data.keys() + + def items(self): + """D.items() -> list of D's (key, value) pairs, as 2-tuples.""" + if not self.loaded: + self.load() + return self._data.items() + + def values(self): + """D.values() -> list of D's values.""" + if not self.loaded: + self.load() + return self._data.values() + + +class RamSession(Session): + + # Class-level objects. Don't rebind these! + cache = {} + locks = {} + + def clean_up(self): + """Clean up expired sessions.""" + + now = self.now() + for _id, (data, expiration_time) in list(six.iteritems(self.cache)): + if expiration_time <= now: + try: + del self.cache[_id] + except KeyError: + pass + try: + if self.locks[_id].acquire(blocking=False): + lock = self.locks.pop(_id) + lock.release() + except KeyError: + pass + + # added to remove obsolete lock objects + for _id in list(self.locks): + locked = ( + _id not in self.cache + and self.locks[_id].acquire(blocking=False) + ) + if locked: + lock = self.locks.pop(_id) + lock.release() + + def _exists(self): + return self.id in self.cache + + def _load(self): + return self.cache.get(self.id) + + def _save(self, expiration_time): + self.cache[self.id] = (self._data, expiration_time) + + def _delete(self): + self.cache.pop(self.id, None) + + def acquire_lock(self): + """Acquire an exclusive lock on the currently-loaded session data.""" + self.locked = True + self.locks.setdefault(self.id, threading.RLock()).acquire() + + def release_lock(self): + """Release the lock on the currently-loaded session data.""" + self.locks[self.id].release() + self.locked = False + + def __len__(self): + """Return the number of active sessions.""" + return len(self.cache) + + +class FileSession(Session): + + """Implementation of the File backend for sessions + + storage_path + The folder where session data will be saved. Each session + will be saved as pickle.dump(data, expiration_time) in its own file; + the filename will be self.SESSION_PREFIX + self.id. + + lock_timeout + A timedelta or numeric seconds indicating how long + to block acquiring a lock. If None (default), acquiring a lock + will block indefinitely. + """ + + SESSION_PREFIX = 'session-' + LOCK_SUFFIX = '.lock' + pickle_protocol = pickle.HIGHEST_PROTOCOL + + def __init__(self, id=None, **kwargs): + # The 'storage_path' arg is required for file-based sessions. + kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) + kwargs.setdefault('lock_timeout', None) + + Session.__init__(self, id=id, **kwargs) + + # validate self.lock_timeout + if isinstance(self.lock_timeout, (int, float)): + self.lock_timeout = datetime.timedelta(seconds=self.lock_timeout) + if not isinstance(self.lock_timeout, (datetime.timedelta, type(None))): + raise ValueError( + 'Lock timeout must be numeric seconds or a timedelta instance.' + ) + + @classmethod + def setup(cls, **kwargs): + """Set up the storage system for file-based sessions. + + This should only be called once per process; this will be done + automatically when using sessions.init (as the built-in Tool does). + """ + # The 'storage_path' arg is required for file-based sessions. + kwargs['storage_path'] = os.path.abspath(kwargs['storage_path']) + + for k, v in kwargs.items(): + setattr(cls, k, v) + + def _get_file_path(self): + f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id) + if not os.path.abspath(f).startswith(self.storage_path): + raise cherrypy.HTTPError(400, 'Invalid session id in cookie.') + return f + + def _exists(self): + path = self._get_file_path() + return os.path.exists(path) + + def _load(self, path=None): + assert self.locked, ('The session load without being locked. ' + "Check your tools' priority levels.") + if path is None: + path = self._get_file_path() + try: + f = open(path, 'rb') + try: + return pickle.load(f) + finally: + f.close() + except (IOError, EOFError): + e = sys.exc_info()[1] + if self.debug: + cherrypy.log('Error loading the session pickle: %s' % + e, 'TOOLS.SESSIONS') + return None + + def _save(self, expiration_time): + assert self.locked, ('The session was saved without being locked. ' + "Check your tools' priority levels.") + f = open(self._get_file_path(), 'wb') + try: + pickle.dump((self._data, expiration_time), f, self.pickle_protocol) + finally: + f.close() + + def _delete(self): + assert self.locked, ('The session deletion without being locked. ' + "Check your tools' priority levels.") + try: + os.unlink(self._get_file_path()) + except OSError: + pass + + def acquire_lock(self, path=None): + """Acquire an exclusive lock on the currently-loaded session data.""" + if path is None: + path = self._get_file_path() + path += self.LOCK_SUFFIX + checker = locking.LockChecker(self.id, self.lock_timeout) + while not checker.expired(): + try: + self.lock = zc.lockfile.LockFile(path) + except zc.lockfile.LockError: + time.sleep(0.1) + else: + break + self.locked = True + if self.debug: + cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS') + + def release_lock(self, path=None): + """Release the lock on the currently-loaded session data.""" + self.lock.close() + with contextlib2.suppress(FileNotFoundError): + os.remove(self.lock._path) + self.locked = False + + def clean_up(self): + """Clean up expired sessions.""" + now = self.now() + # Iterate over all session files in self.storage_path + for fname in os.listdir(self.storage_path): + have_session = ( + fname.startswith(self.SESSION_PREFIX) + and not fname.endswith(self.LOCK_SUFFIX) + ) + if have_session: + # We have a session file: lock and load it and check + # if it's expired. If it fails, nevermind. + path = os.path.join(self.storage_path, fname) + self.acquire_lock(path) + if self.debug: + # This is a bit of a hack, since we're calling clean_up + # on the first instance rather than the entire class, + # so depending on whether you have "debug" set on the + # path of the first session called, this may not run. + cherrypy.log('Cleanup lock acquired.', 'TOOLS.SESSIONS') + + try: + contents = self._load(path) + # _load returns None on IOError + if contents is not None: + data, expiration_time = contents + if expiration_time < now: + # Session expired: deleting it + os.unlink(path) + finally: + self.release_lock(path) + + def __len__(self): + """Return the number of active sessions.""" + return len([fname for fname in os.listdir(self.storage_path) + if (fname.startswith(self.SESSION_PREFIX) and + not fname.endswith(self.LOCK_SUFFIX))]) + + +class MemcachedSession(Session): + + # The most popular memcached client for Python isn't thread-safe. + # Wrap all .get and .set operations in a single lock. + mc_lock = threading.RLock() + + # This is a separate set of locks per session id. + locks = {} + + servers = ['127.0.0.1:11211'] + + @classmethod + def setup(cls, **kwargs): + """Set up the storage system for memcached-based sessions. + + This should only be called once per process; this will be done + automatically when using sessions.init (as the built-in Tool does). + """ + for k, v in kwargs.items(): + setattr(cls, k, v) + + import memcache + cls.cache = memcache.Client(cls.servers) + + def _exists(self): + self.mc_lock.acquire() + try: + return bool(self.cache.get(self.id)) + finally: + self.mc_lock.release() + + def _load(self): + self.mc_lock.acquire() + try: + return self.cache.get(self.id) + finally: + self.mc_lock.release() + + def _save(self, expiration_time): + # Send the expiration time as "Unix time" (seconds since 1/1/1970) + td = int(time.mktime(expiration_time.timetuple())) + self.mc_lock.acquire() + try: + if not self.cache.set(self.id, (self._data, expiration_time), td): + raise AssertionError( + 'Session data for id %r not set.' % self.id) + finally: + self.mc_lock.release() + + def _delete(self): + self.cache.delete(self.id) + + def acquire_lock(self): + """Acquire an exclusive lock on the currently-loaded session data.""" + self.locked = True + self.locks.setdefault(self.id, threading.RLock()).acquire() + if self.debug: + cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS') + + def release_lock(self): + """Release the lock on the currently-loaded session data.""" + self.locks[self.id].release() + self.locked = False + + def __len__(self): + """Return the number of active sessions.""" + raise NotImplementedError + + +# Hook functions (for CherryPy tools) + +def save(): + """Save any changed session data.""" + + if not hasattr(cherrypy.serving, 'session'): + return + request = cherrypy.serving.request + response = cherrypy.serving.response + + # Guard against running twice + if hasattr(request, '_sessionsaved'): + return + request._sessionsaved = True + + if response.stream: + # If the body is being streamed, we have to save the data + # *after* the response has been written out + request.hooks.attach('on_end_request', cherrypy.session.save) + else: + # If the body is not being streamed, we save the data now + # (so we can release the lock). + if is_iterator(response.body): + response.collapse_body() + cherrypy.session.save() + + +save.failsafe = True + + +def close(): + """Close the session object for this request.""" + sess = getattr(cherrypy.serving, 'session', None) + if getattr(sess, 'locked', False): + # If the session is still locked we release the lock + sess.release_lock() + if sess.debug: + cherrypy.log('Lock released on close.', 'TOOLS.SESSIONS') + + +close.failsafe = True +close.priority = 90 + + +def init(storage_type=None, path=None, path_header=None, name='session_id', + timeout=60, domain=None, secure=False, clean_freq=5, + persistent=True, httponly=False, debug=False, + # Py27 compat + # *, storage_class=RamSession, + **kwargs): + """Initialize session object (using cookies). + + storage_class + The Session subclass to use. Defaults to RamSession. + + storage_type + (deprecated) + One of 'ram', 'file', memcached'. This will be + used to look up the corresponding class in cherrypy.lib.sessions + globals. For example, 'file' will use the FileSession class. + + path + The 'path' value to stick in the response cookie metadata. + + path_header + If 'path' is None (the default), then the response + cookie 'path' will be pulled from request.headers[path_header]. + + name + The name of the cookie. + + timeout + The expiration timeout (in minutes) for the stored session data. + If 'persistent' is True (the default), this is also the timeout + for the cookie. + + domain + The cookie domain. + + secure + If False (the default) the cookie 'secure' value will not + be set. If True, the cookie 'secure' value will be set (to 1). + + clean_freq (minutes) + The poll rate for expired session cleanup. + + persistent + If True (the default), the 'timeout' argument will be used + to expire the cookie. If False, the cookie will not have an expiry, + and the cookie will be a "session cookie" which expires when the + browser is closed. + + httponly + If False (the default) the cookie 'httponly' value will not be set. + If True, the cookie 'httponly' value will be set (to 1). + + Any additional kwargs will be bound to the new Session instance, + and may be specific to the storage type. See the subclass of Session + you're using for more information. + """ + + # Py27 compat + storage_class = kwargs.pop('storage_class', RamSession) + + request = cherrypy.serving.request + + # Guard against running twice + if hasattr(request, '_session_init_flag'): + return + request._session_init_flag = True + + # Check if request came with a session ID + id = None + if name in request.cookie: + id = request.cookie[name].value + if debug: + cherrypy.log('ID obtained from request.cookie: %r' % id, + 'TOOLS.SESSIONS') + + first_time = not hasattr(cherrypy, 'session') + + if storage_type: + if first_time: + msg = 'storage_type is deprecated. Supply storage_class instead' + cherrypy.log(msg) + storage_class = storage_type.title() + 'Session' + storage_class = globals()[storage_class] + + # call setup first time only + if first_time: + if hasattr(storage_class, 'setup'): + storage_class.setup(**kwargs) + + # Create and attach a new Session instance to cherrypy.serving. + # It will possess a reference to (and lock, and lazily load) + # the requested session data. + kwargs['timeout'] = timeout + kwargs['clean_freq'] = clean_freq + cherrypy.serving.session = sess = storage_class(id, **kwargs) + sess.debug = debug + + def update_cookie(id): + """Update the cookie every time the session id changes.""" + cherrypy.serving.response.cookie[name] = id + sess.id_observers.append(update_cookie) + + # Create cherrypy.session which will proxy to cherrypy.serving.session + if not hasattr(cherrypy, 'session'): + cherrypy.session = cherrypy._ThreadLocalProxy('session') + + if persistent: + cookie_timeout = timeout + else: + # See http://support.microsoft.com/kb/223799/EN-US/ + # and http://support.mozilla.com/en-US/kb/Cookies + cookie_timeout = None + set_response_cookie(path=path, path_header=path_header, name=name, + timeout=cookie_timeout, domain=domain, secure=secure, + httponly=httponly) + + +def set_response_cookie(path=None, path_header=None, name='session_id', + timeout=60, domain=None, secure=False, httponly=False): + """Set a response cookie for the client. + + path + the 'path' value to stick in the response cookie metadata. + + path_header + if 'path' is None (the default), then the response + cookie 'path' will be pulled from request.headers[path_header]. + + name + the name of the cookie. + + timeout + the expiration timeout for the cookie. If 0 or other boolean + False, no 'expires' param will be set, and the cookie will be a + "session cookie" which expires when the browser is closed. + + domain + the cookie domain. + + secure + if False (the default) the cookie 'secure' value will not + be set. If True, the cookie 'secure' value will be set (to 1). + + httponly + If False (the default) the cookie 'httponly' value will not be set. + If True, the cookie 'httponly' value will be set (to 1). + + """ + # Set response cookie + cookie = cherrypy.serving.response.cookie + cookie[name] = cherrypy.serving.session.id + cookie[name]['path'] = ( + path or + cherrypy.serving.request.headers.get(path_header) or + '/' + ) + + if timeout: + cookie[name]['max-age'] = timeout * 60 + _add_MSIE_max_age_workaround(cookie[name], timeout) + if domain is not None: + cookie[name]['domain'] = domain + if secure: + cookie[name]['secure'] = 1 + if httponly: + if not cookie[name].isReservedKey('httponly'): + raise ValueError('The httponly cookie token is not supported.') + cookie[name]['httponly'] = 1 + + +def _add_MSIE_max_age_workaround(cookie, timeout): + """ + We'd like to use the "max-age" param as indicated in + http://www.faqs.org/rfcs/rfc2109.html but IE doesn't + save it to disk and the session is lost if people close + the browser. So we have to use the old "expires" ... sigh ... + """ + expires = time.time() + timeout * 60 + cookie['expires'] = httputil.HTTPDate(expires) + + +def expire(): + """Expire the current session cookie.""" + name = cherrypy.serving.request.config.get( + 'tools.sessions.name', 'session_id') + one_year = 60 * 60 * 24 * 365 + e = time.time() - one_year + cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e) + cherrypy.serving.response.cookie[name].pop('max-age', None) diff --git a/resources/lib/cherrypy/lib/static.py b/resources/lib/cherrypy/lib/static.py new file mode 100644 index 0000000..da9d937 --- /dev/null +++ b/resources/lib/cherrypy/lib/static.py @@ -0,0 +1,390 @@ +"""Module with helpers for serving static files.""" + +import os +import platform +import re +import stat +import mimetypes + +from email.generator import _make_boundary as make_boundary +from io import UnsupportedOperation + +from six.moves import urllib + +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy.lib import cptools, httputil, file_generator_limited + + +def _setup_mimetypes(): + """Pre-initialize global mimetype map.""" + if not mimetypes.inited: + mimetypes.init() + mimetypes.types_map['.dwg'] = 'image/x-dwg' + mimetypes.types_map['.ico'] = 'image/x-icon' + mimetypes.types_map['.bz2'] = 'application/x-bzip2' + mimetypes.types_map['.gz'] = 'application/x-gzip' + + +_setup_mimetypes() + + +def serve_file(path, content_type=None, disposition=None, name=None, + debug=False): + """Set status, headers, and body in order to serve the given path. + + The Content-Type header will be set to the content_type arg, if provided. + If not provided, the Content-Type will be guessed by the file extension + of the 'path' argument. + + If disposition is not None, the Content-Disposition header will be set + to "; filename=". If name is None, it will be set + to the basename of path. If disposition is None, no Content-Disposition + header will be written. + """ + response = cherrypy.serving.response + + # If path is relative, users should fix it by making path absolute. + # That is, CherryPy should not guess where the application root is. + # It certainly should *not* use cwd (since CP may be invoked from a + # variety of paths). If using tools.staticdir, you can make your relative + # paths become absolute by supplying a value for "tools.staticdir.root". + if not os.path.isabs(path): + msg = "'%s' is not an absolute path." % path + if debug: + cherrypy.log(msg, 'TOOLS.STATICFILE') + raise ValueError(msg) + + try: + st = os.stat(path) + except (OSError, TypeError, ValueError): + # OSError when file fails to stat + # TypeError on Python 2 when there's a null byte + # ValueError on Python 3 when there's a null byte + if debug: + cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC') + raise cherrypy.NotFound() + + # Check if path is a directory. + if stat.S_ISDIR(st.st_mode): + # Let the caller deal with it as they like. + if debug: + cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC') + raise cherrypy.NotFound() + + # Set the Last-Modified response header, so that + # modified-since validation code can work. + response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime) + cptools.validate_since() + + if content_type is None: + # Set content-type based on filename extension + ext = '' + i = path.rfind('.') + if i != -1: + ext = path[i:].lower() + content_type = mimetypes.types_map.get(ext, None) + if content_type is not None: + response.headers['Content-Type'] = content_type + if debug: + cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC') + + cd = None + if disposition is not None: + if name is None: + name = os.path.basename(path) + cd = '%s; filename="%s"' % (disposition, name) + response.headers['Content-Disposition'] = cd + if debug: + cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') + + # Set Content-Length and use an iterable (file object) + # this way CP won't load the whole file in memory + content_length = st.st_size + fileobj = open(path, 'rb') + return _serve_fileobj(fileobj, content_type, content_length, debug=debug) + + +def serve_fileobj(fileobj, content_type=None, disposition=None, name=None, + debug=False): + """Set status, headers, and body in order to serve the given file object. + + The Content-Type header will be set to the content_type arg, if provided. + + If disposition is not None, the Content-Disposition header will be set + to "; filename=". If name is None, 'filename' will + not be set. If disposition is None, no Content-Disposition header will + be written. + + CAUTION: If the request contains a 'Range' header, one or more seek()s will + be performed on the file object. This may cause undesired behavior if + the file object is not seekable. It could also produce undesired results + if the caller set the read position of the file object prior to calling + serve_fileobj(), expecting that the data would be served starting from that + position. + """ + response = cherrypy.serving.response + + try: + st = os.fstat(fileobj.fileno()) + except AttributeError: + if debug: + cherrypy.log('os has no fstat attribute', 'TOOLS.STATIC') + content_length = None + except UnsupportedOperation: + content_length = None + else: + # Set the Last-Modified response header, so that + # modified-since validation code can work. + response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime) + cptools.validate_since() + content_length = st.st_size + + if content_type is not None: + response.headers['Content-Type'] = content_type + if debug: + cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC') + + cd = None + if disposition is not None: + if name is None: + cd = disposition + else: + cd = '%s; filename="%s"' % (disposition, name) + response.headers['Content-Disposition'] = cd + if debug: + cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC') + + return _serve_fileobj(fileobj, content_type, content_length, debug=debug) + + +def _serve_fileobj(fileobj, content_type, content_length, debug=False): + """Internal. Set response.body to the given file object, perhaps ranged.""" + response = cherrypy.serving.response + + # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code + request = cherrypy.serving.request + if request.protocol >= (1, 1): + response.headers['Accept-Ranges'] = 'bytes' + r = httputil.get_ranges(request.headers.get('Range'), content_length) + if r == []: + response.headers['Content-Range'] = 'bytes */%s' % content_length + message = ('Invalid Range (first-byte-pos greater than ' + 'Content-Length)') + if debug: + cherrypy.log(message, 'TOOLS.STATIC') + raise cherrypy.HTTPError(416, message) + + if r: + if len(r) == 1: + # Return a single-part response. + start, stop = r[0] + if stop > content_length: + stop = content_length + r_len = stop - start + if debug: + cherrypy.log( + 'Single part; start: %r, stop: %r' % (start, stop), + 'TOOLS.STATIC') + response.status = '206 Partial Content' + response.headers['Content-Range'] = ( + 'bytes %s-%s/%s' % (start, stop - 1, content_length)) + response.headers['Content-Length'] = r_len + fileobj.seek(start) + response.body = file_generator_limited(fileobj, r_len) + else: + # Return a multipart/byteranges response. + response.status = '206 Partial Content' + boundary = make_boundary() + ct = 'multipart/byteranges; boundary=%s' % boundary + response.headers['Content-Type'] = ct + if 'Content-Length' in response.headers: + # Delete Content-Length header so finalize() recalcs it. + del response.headers['Content-Length'] + + def file_ranges(): + # Apache compatibility: + yield b'\r\n' + + for start, stop in r: + if debug: + cherrypy.log( + 'Multipart; start: %r, stop: %r' % ( + start, stop), + 'TOOLS.STATIC') + yield ntob('--' + boundary, 'ascii') + yield ntob('\r\nContent-type: %s' % content_type, + 'ascii') + yield ntob( + '\r\nContent-range: bytes %s-%s/%s\r\n\r\n' % ( + start, stop - 1, content_length), + 'ascii') + fileobj.seek(start) + gen = file_generator_limited(fileobj, stop - start) + for chunk in gen: + yield chunk + yield b'\r\n' + # Final boundary + yield ntob('--' + boundary + '--', 'ascii') + + # Apache compatibility: + yield b'\r\n' + response.body = file_ranges() + return response.body + else: + if debug: + cherrypy.log('No byteranges requested', 'TOOLS.STATIC') + + # Set Content-Length and use an iterable (file object) + # this way CP won't load the whole file in memory + response.headers['Content-Length'] = content_length + response.body = fileobj + return response.body + + +def serve_download(path, name=None): + """Serve 'path' as an application/x-download attachment.""" + # This is such a common idiom I felt it deserved its own wrapper. + return serve_file(path, 'application/x-download', 'attachment', name) + + +def _attempt(filename, content_types, debug=False): + if debug: + cherrypy.log('Attempting %r (content_types %r)' % + (filename, content_types), 'TOOLS.STATICDIR') + try: + # you can set the content types for a + # complete directory per extension + content_type = None + if content_types: + r, ext = os.path.splitext(filename) + content_type = content_types.get(ext[1:], None) + serve_file(filename, content_type=content_type, debug=debug) + return True + except cherrypy.NotFound: + # If we didn't find the static file, continue handling the + # request. We might find a dynamic handler instead. + if debug: + cherrypy.log('NotFound', 'TOOLS.STATICFILE') + return False + + +def staticdir(section, dir, root='', match='', content_types=None, index='', + debug=False): + """Serve a static resource from the given (root +) dir. + + match + If given, request.path_info will be searched for the given + regular expression before attempting to serve static content. + + content_types + If given, it should be a Python dictionary of + {file-extension: content-type} pairs, where 'file-extension' is + a string (e.g. "gif") and 'content-type' is the value to write + out in the Content-Type response header (e.g. "image/gif"). + + index + If provided, it should be the (relative) name of a file to + serve for directory requests. For example, if the dir argument is + '/home/me', the Request-URI is 'myapp', and the index arg is + 'index.html', the file '/home/me/myapp/index.html' will be sought. + """ + request = cherrypy.serving.request + if request.method not in ('GET', 'HEAD'): + if debug: + cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR') + return False + + if match and not re.search(match, request.path_info): + if debug: + cherrypy.log('request.path_info %r does not match pattern %r' % + (request.path_info, match), 'TOOLS.STATICDIR') + return False + + # Allow the use of '~' to refer to a user's home directory. + dir = os.path.expanduser(dir) + + # If dir is relative, make absolute using "root". + if not os.path.isabs(dir): + if not root: + msg = 'Static dir requires an absolute dir (or root).' + if debug: + cherrypy.log(msg, 'TOOLS.STATICDIR') + raise ValueError(msg) + dir = os.path.join(root, dir) + + # Determine where we are in the object tree relative to 'section' + # (where the static tool was defined). + if section == 'global': + section = '/' + section = section.rstrip(r'\/') + branch = request.path_info[len(section) + 1:] + branch = urllib.parse.unquote(branch.lstrip(r'\/')) + + # Requesting a file in sub-dir of the staticdir results + # in mixing of delimiter styles, e.g. C:\static\js/script.js. + # Windows accepts this form except not when the path is + # supplied in extended-path notation, e.g. \\?\C:\static\js/script.js. + # http://bit.ly/1vdioCX + if platform.system() == 'Windows': + branch = branch.replace('/', '\\') + + # If branch is "", filename will end in a slash + filename = os.path.join(dir, branch) + if debug: + cherrypy.log('Checking file %r to fulfill %r' % + (filename, request.path_info), 'TOOLS.STATICDIR') + + # There's a chance that the branch pulled from the URL might + # have ".." or similar uplevel attacks in it. Check that the final + # filename is a child of dir. + if not os.path.normpath(filename).startswith(os.path.normpath(dir)): + raise cherrypy.HTTPError(403) # Forbidden + + handled = _attempt(filename, content_types) + if not handled: + # Check for an index file if a folder was requested. + if index: + handled = _attempt(os.path.join(filename, index), content_types) + if handled: + request.is_index = filename[-1] in (r'\/') + return handled + + +def staticfile(filename, root=None, match='', content_types=None, debug=False): + """Serve a static resource from the given (root +) filename. + + match + If given, request.path_info will be searched for the given + regular expression before attempting to serve static content. + + content_types + If given, it should be a Python dictionary of + {file-extension: content-type} pairs, where 'file-extension' is + a string (e.g. "gif") and 'content-type' is the value to write + out in the Content-Type response header (e.g. "image/gif"). + + """ + request = cherrypy.serving.request + if request.method not in ('GET', 'HEAD'): + if debug: + cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE') + return False + + if match and not re.search(match, request.path_info): + if debug: + cherrypy.log('request.path_info %r does not match pattern %r' % + (request.path_info, match), 'TOOLS.STATICFILE') + return False + + # If filename is relative, make absolute using "root". + if not os.path.isabs(filename): + if not root: + msg = "Static tool requires an absolute filename (got '%s')." % ( + filename,) + if debug: + cherrypy.log(msg, 'TOOLS.STATICFILE') + raise ValueError(msg) + filename = os.path.join(root, filename) + + return _attempt(filename, content_types, debug=debug) diff --git a/resources/lib/cherrypy/lib/xmlrpcutil.py b/resources/lib/cherrypy/lib/xmlrpcutil.py new file mode 100644 index 0000000..ddaac86 --- /dev/null +++ b/resources/lib/cherrypy/lib/xmlrpcutil.py @@ -0,0 +1,61 @@ +"""XML-RPC tool helpers.""" +import sys + +from six.moves.xmlrpc_client import ( + loads as xmlrpc_loads, dumps as xmlrpc_dumps, + Fault as XMLRPCFault +) + +import cherrypy +from cherrypy._cpcompat import ntob + + +def process_body(): + """Return (params, method) from request body.""" + try: + return xmlrpc_loads(cherrypy.request.body.read()) + except Exception: + return ('ERROR PARAMS', ), 'ERRORMETHOD' + + +def patched_path(path): + """Return 'path', doctored for RPC.""" + if not path.endswith('/'): + path += '/' + if path.startswith('/RPC2/'): + # strip the first /rpc2 + path = path[5:] + return path + + +def _set_response(body): + """Set up HTTP status, headers and body within CherryPy.""" + # The XML-RPC spec (http://www.xmlrpc.com/spec) says: + # "Unless there's a lower-level error, always return 200 OK." + # Since Python's xmlrpc_client interprets a non-200 response + # as a "Protocol Error", we'll just return 200 every time. + response = cherrypy.response + response.status = '200 OK' + response.body = ntob(body, 'utf-8') + response.headers['Content-Type'] = 'text/xml' + response.headers['Content-Length'] = len(body) + + +def respond(body, encoding='utf-8', allow_none=0): + """Construct HTTP response body.""" + if not isinstance(body, XMLRPCFault): + body = (body,) + + _set_response( + xmlrpc_dumps( + body, methodresponse=1, + encoding=encoding, + allow_none=allow_none + ) + ) + + +def on_error(*args, **kwargs): + """Construct HTTP response body for an error response.""" + body = str(sys.exc_info()[1]) + _set_response(xmlrpc_dumps(XMLRPCFault(1, body))) diff --git a/resources/lib/cherrypy/process/__init__.py b/resources/lib/cherrypy/process/__init__.py new file mode 100644 index 0000000..f242d22 --- /dev/null +++ b/resources/lib/cherrypy/process/__init__.py @@ -0,0 +1,17 @@ +"""Site container for an HTTP server. + +A Web Site Process Bus object is used to connect applications, servers, +and frameworks with site-wide services such as daemonization, process +reload, signal handling, drop privileges, PID file management, logging +for all of these, and many more. + +The 'plugins' module defines a few abstract and concrete services for +use with the bus. Some use tool-specific channels; see the documentation +for each class. +""" + +from .wspbus import bus +from . import plugins, servers + + +__all__ = ('bus', 'plugins', 'servers') diff --git a/resources/lib/cherrypy/process/__pycache__/__init__.cpython-37.opt-1.pyc b/resources/lib/cherrypy/process/__pycache__/__init__.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5a4f76ce6d0821474ff0854d697a52fce088c3b2 GIT binary patch literal 769 zcmYjPU5eB|5YEr+%o1Jn2n9Z@1G71Rin!}OtOznVBSMITo^)l>+UX8m$%fgZcmuEC zQS$1OSMbT|%m_B5NOivYs=lvsb8(Rbe1`8&zC4S9;FnMS9nOO{{Lpg_GROiM%1DPz zltrO`k9FM4vRQMMopCWSqH+v-FCYMLU)fYO_FQ) zj60Z+YiCO&f}5Ve?!IE#fg{lS2Z5gHRmBu}C z{Gl(Tawx z%->t7av>#eJ3mg5ie4(4QfoU^9dd_cd&j%Ywc6!njm{m~Lw?$5UKGlxt|-#>FrL3U S8vXha_oX?9S@bx38vg}uY4q0s literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/process/__pycache__/plugins.cpython-37.opt-1.pyc b/resources/lib/cherrypy/process/__pycache__/plugins.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7de1e33700a8df013974e06df66dd09ba71e6b2d GIT binary patch literal 22629 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 literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/process/__pycache__/servers.cpython-37.opt-1.pyc b/resources/lib/cherrypy/process/__pycache__/servers.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..681cfc1188a3ce6c1997bb1dc3dc0064791f0a21 GIT binary patch literal 12081 zcmc&)OLH4ncJ2pWY=RUm%aZKzB-fN}5-JjuJWh?nNE&G*QC4hW6qB~b9NIt=-5{H6 zpyAsMQGh9@VmYarMVzWsvdB~xR8=XqvSi*)sAvJHqRTo4c-* z{ri3Kqw3`)(d4^szZ$yop(}+K3a{Pu-L~6_Tu0cUushbObX%*9+npV+&>njqPor_HI~G|uGie95p5CnwzxxE0b(=nI2=329?~WUKI~`0W3PdyLL^AM25Mkaz z5fm=bG!ILts#*tLv@2X*v?{+^U9~KMUwBzu6=UpL6}}r? zd&`2fVR>yj{4fw}ey_VFE?!)_cyUXhi6Wa9C4IF%{)z?Dg`PpN+JQ5oBNKv7q78Xr znSuzknmb-1NK2v>Kms1wq^e8Sfwa353MjmYl-(X*=Tri0$B|f`#gQd^`_iRk{aSbv zvW|vTn!;K(t3|up^}VJYc|qrD#>_~%?vmIB18<;PDC$+}K3Rm3z7%H?1zpUVCxRw+ zqbNa$t1Z=Co%$K=Y-~YkA;hW^G*bpTS5_Ri>j(Xvp69qLY5?b2LvgUz6(k+ zFPwmMM%dfILzkFoks3erZ161bvYW-b0;|H_<;?4gzhoyq02@K z%o0~<9;+)Wk+hrD5Hs+q&7i$P@2)lKG>i;gA2U4IbwPpTetRJySVD250VojA1i%pl zv)+O%V(C{$r8JSxOi-<5^|P!S0x;eeA)`3fF#=7#i%HSQiFym!>wpb_hoqmX^)aTu z*9)Tr%D@_)jqhzsTlSYMz&+tK8g|Hz+~@**Qjipj+*hLkX-=taG%7R;-`gR6IEg@E zMmO2u!2C1=$F)8k{aMy7Kscy&`>C8>cx|N@$`#1gO1B^F1|0?%ObvLGL6rhEWj@}e z4ju&3cUCjxT3A?EXGH;ae%5m_M?kaVgZ12|iuYOF>uL>P58X%IAat`Aj@wcYu}G^| z8TL+~DZypg5%WB{IH2KQn@`B%>7qZ-&i^dz;sG)hIusA0f;CNv`+X0GM!RC`^=j@-r17ZNKl?7JFt7FO2)VD4kU zA2g}MGNIJzBsFR%d7;r4mRk$JznyfE)ToYFZi*}Iz8dl0qFus~rtt+aQR&>in?w|UExVkK&Kd8*0d1Z`KkcC@<^1&PvE87h}O zEb?;XwcVf>iOX+Y63Yp|0({@b_)2$y6-)=1?mo0Tk@(1y5p3IU_-U4n=73Amc5bXh5K`^*d^c<;SF8hQQxzC-8jaVL~f^AnArN6r0ZKWN&%YDtX}-Jh(RJZ92hT~+H3dSCzp$n}FTH@FWeYyffyzjd7~>;VA*TeY?YM3eiF$1TYUhfM_Oi$=fce zcUZNaA%fFtO)Ar$j*II!!ZRpb4N;v&5gnpA1CgAG=qeYNHWa*s&+@4BjGELc`M6ZC zckH%Xug6xsuG9n9<$C=y088?w6f~Q?uID;&t_9oh_nM?YlZ*e|xw*RaA+#&p^25Q_ zjc)gr9obtS1U7iGv-Mu!cw6u+K=_sl!>V@A@q%jD4I;1A-+~hc0Y%KT1^c~~T7|6y z{kD|ahOo2Jz>t4}BcxEaq~~;gGR6iIZ?037FHnrJ;RhVN>R<+ktq5y5T-_|FFmeyG*8Lj$FLXreS64JC3YD07Ymy)$ZSP%;yp#?~-wh^Ht9;h-U& z@{qoeve%8`V&bzxTXW>QNhCxxIZMSUDl)B;)3_zo^*UZuum3gfLeBl@h2%dtJ0^YA zv}%*|o_=(u3GrlJ$Hk$xryU_3gtPI~(9?TNZqKB9o{m|I^StaJ5Z+KF$0Wv6Swt%d zf#gdlD!M#P*W*OXvv~HmI9QD}eNG>o8>cku>U)zqLQ6vDbgz?jKl1iuO3#7HqcKWf z9;bAqvy4`PAi`H>>p$Xx*eYK|sUHE6iJ51$r#je)oR>qAlf$!ocquA4<`ex(Ju2>% zj;5TPgGC5GjV!#Kj>=CA`Drw>H%oU;K038G>*-%pF9l@r<{s;Z`rZq8ciJibx&GKV zG>%?$N+{1f)z0x~xPIv=*8EUAI!k9~3L_J3L=}xOLRDoN!o!g~S));m8oYGFm#*y~ zW<$_T!4+~tZ-ljhs0E`H5w(Pf2H>eTQ60&-YHYoKcYWh_t$zRRt+<#tx{_qDqQ?ac zw&O;F>0vHKYz!DavoR{?6_GImvA73CbgBSnd=k8Ho^-j*_uu@QuE z(d|6+WYCFC3=t#AmZtt#rQ$q-PT!4-gqQFK

#8N*+%|yEIo&5EnN1GCo~r*{_o# z*VXH|h`6-|0~#06KV7g~#km99iz=p40aCA6k0w=tRORn*gaUND79fm&8pR>Yop6!D7FVKd8x%x&J=jg$+`=Dk}d?Q4H1`IM2>1IYN!$AUPN09{c z2x!p^VN0ANNsbF4Tq(%n1o$qY-hbc-357LqDyLhJv78P`GpH;ZlW?4^L&h`G+7b@t z*}vnWPp|~rLBR4r>-<9wBDQbIH68bCN6aH^ClKgG`J=*9aPnnvw_Zdm^Qc6vKGr&q z&}s^;z8Gs|ep9P2soiz7v(WBeRlDOO?BxKa@cICaWH&81YP@-AD4wdRmFhtiSC>Wq&R!u18CP?;iyQ`{?q_Y<=3^SgxXV)^~4C@=lf~gm%rBj;uYwkawp18 zVdzdgz_#8WX&-3N3bd{i2k$(98576^dOnNM$m+0svNJFu;7Qsttj2kE6xMcr^*8kU z=by;)m`hE*jIuH(e}t1-GPMHa5x}$bUAHTLhu-}z6@m)332YtMFj)1_Zn_Xh=(5{XP@TrpFe-EjOYNra z3%L^V_o$dKC&Y>Za5E(KYXwW617`>4$2mE)SQ$Ubf*iBD@(+oJM+Vu0h+L9fl!v1N z$9Rt{Maj{lsl8(2_LAdFt6&{Xv-_eer@C`Gc}7lk@{A?9FnDR5vbV&X5gD$sJgyDS z^D{*^+>s&t4OH#n8#{G?Q-G{h%E~k4FpUDA35it#%6=uwD+$sNke(y2)X!<+EcFW4 z(6GoMgv_2XCMA{PuxF%JW9-eul^_wmo$|n$$rQT&uhwDK3%)DvBuLV!e(nv3gy;NRP-PkrZ+% z|GCtaULd`wALA;3Esgq;JUvM+j3oG#q|*ulnASUVI}}j9qA*#KU-)M@LTX={$r)C$ zl*9jA!6?s^PL*CRohg?~<$}RAo2Zoih>O>8gk*Wf5=w?+BGtpL1L4+|5Ucv=`9HO=L$2x?I zGY8N+^J`oo{Xq$lJnnPk^C9HSf!sXlVJdQ9P!d8^(@@YF@?xV_DQ=#k{i6|H$r?tL z*`zk7z`u}1j%OX4@Ya%mK~36HbTRu0g^}-~u2L47hSJdC{P>EOmV&}J(@B<$ zoKeb`EJ;s)E5bcX#141b2N$0&-L$!NqIeTv(81z;9T!MYkThveA=U_3FH*GS>yJ&u zUM3PFMtCv8=SP$vVMWMs(k+kZORE=H5el`LP&KD3n?%|c?(5cgw>{?P5zJ`WrD1PMFnyrPpQdCiu+Sq-8sCZ z1RA-RhF&mPh0l#I&bR0=_5aN}OrLy_4%hQL>Re!+lSR&{*v%p0kU@Cxd+k*N;lnnXG-UGeSqwqsxW1nvF@Pqe|C6EkJ4tzO|xsqh*uR0F`s z)aG1k1V%_GCvYUT=%E6V_$)#Lbmsf(_!bMBt7^j(QA0iOo%OqS6u?Rn-EV`M8m$xA z9}RBioK?nJ0cOvfMES{hGmoa6HKYtPoX$OCkFzy<1^|{|HvernRFEH{4{Yx2!?(T@ zP&#Wm$z9zK0)Nx0)86w<+U<`^ZTr!B7xCY>Vb?aziw%caGU4(ufDR!6I|JVf!0vGa4Xzq3x$Ivvw#dYCu|AE9^ofQsu> zd>7sA(E^;TyOR{2th?lHCcXmcu8!RyN+jxgCOJm{&D?W>k~KD7*ql|wZV|;lr{ZHO zHc@G4!yx@glkDLrR z@chvl(b=Z&{en&vxg!@;QiThbFJHQ(QgXGy%7-2HTtF4Rqr+x8hEe&*5n4F<=4~1Y zdAfkrh>OXG0GrcI-@~`lby9}5(N$)DBl)uW^oIjwhZcy#;OoqO~D E0QUwXEC2ui literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/process/__pycache__/win32.cpython-37.opt-1.pyc b/resources/lib/cherrypy/process/__pycache__/win32.cpython-37.opt-1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e90957ff3b469dfdca3689219d1322af292cc7cb GIT binary patch literal 6081 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, usually cherrypy.engine. + """ + + def __init__(self, bus): + self.bus = bus + + def subscribe(self): + """Register this object as a (multi-channel) listener on the bus.""" + for channel in self.bus.listeners: + # Subscribe self.start, self.exit, etc. if present. + method = getattr(self, channel, None) + if method is not None: + self.bus.subscribe(channel, method) + + def unsubscribe(self): + """Unregister this object as a listener on the bus.""" + for channel in self.bus.listeners: + # Unsubscribe self.start, self.exit, etc. if present. + method = getattr(self, channel, None) + if method is not None: + self.bus.unsubscribe(channel, method) + + +class SignalHandler(object): + + """Register bus channels (and listeners) for system signals. + + You can modify what signals your application listens for, and what it does + when it receives signals, by modifying :attr:`SignalHandler.handlers`, + a dict of {signal name: callback} pairs. The default set is:: + + handlers = {'SIGTERM': self.bus.exit, + 'SIGHUP': self.handle_SIGHUP, + 'SIGUSR1': self.bus.graceful, + } + + The :func:`SignalHandler.handle_SIGHUP`` method calls + :func:`bus.restart()` + if the process is daemonized, but + :func:`bus.exit()` + if the process is attached to a TTY. This is because Unix window + managers tend to send SIGHUP to terminal windows when the user closes them. + + Feel free to add signals which are not available on every platform. + The :class:`SignalHandler` will ignore errors raised from attempting + to register handlers for unknown signals. + """ + + handlers = {} + """A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit).""" + + signals = {} + """A map from signal numbers to names.""" + + for k, v in vars(_signal).items(): + if k.startswith('SIG') and not k.startswith('SIG_'): + signals[v] = k + del k, v + + def __init__(self, bus): + self.bus = bus + # Set default handlers + self.handlers = {'SIGTERM': self.bus.exit, + 'SIGHUP': self.handle_SIGHUP, + 'SIGUSR1': self.bus.graceful, + } + + if sys.platform[:4] == 'java': + del self.handlers['SIGUSR1'] + self.handlers['SIGUSR2'] = self.bus.graceful + self.bus.log('SIGUSR1 cannot be set on the JVM platform. ' + 'Using SIGUSR2 instead.') + self.handlers['SIGINT'] = self._jython_SIGINT_handler + + self._previous_handlers = {} + # used to determine is the process is a daemon in `self._is_daemonized` + self._original_pid = os.getpid() + + def _jython_SIGINT_handler(self, signum=None, frame=None): + # See http://bugs.jython.org/issue1313 + self.bus.log('Keyboard Interrupt: shutting down bus') + self.bus.exit() + + def _is_daemonized(self): + """Return boolean indicating if the current process is + running as a daemon. + + The criteria to determine the `daemon` condition is to verify + if the current pid is not the same as the one that got used on + the initial construction of the plugin *and* the stdin is not + connected to a terminal. + + The sole validation of the tty is not enough when the plugin + is executing inside other process like in a CI tool + (Buildbot, Jenkins). + """ + return ( + self._original_pid != os.getpid() and + not os.isatty(sys.stdin.fileno()) + ) + + def subscribe(self): + """Subscribe self.handlers to signals.""" + for sig, func in self.handlers.items(): + try: + self.set_handler(sig, func) + except ValueError: + pass + + def unsubscribe(self): + """Unsubscribe self.handlers from signals.""" + for signum, handler in self._previous_handlers.items(): + signame = self.signals[signum] + + if handler is None: + self.bus.log('Restoring %s handler to SIG_DFL.' % signame) + handler = _signal.SIG_DFL + else: + self.bus.log('Restoring %s handler %r.' % (signame, handler)) + + try: + our_handler = _signal.signal(signum, handler) + if our_handler is None: + self.bus.log('Restored old %s handler %r, but our ' + 'handler was not registered.' % + (signame, handler), level=30) + except ValueError: + self.bus.log('Unable to restore %s handler %r.' % + (signame, handler), level=40, traceback=True) + + def set_handler(self, signal, listener=None): + """Subscribe a handler for the given signal (number or name). + + If the optional 'listener' argument is provided, it will be + subscribed as a listener for the given signal's channel. + + If the given signal name or number is not available on the current + platform, ValueError is raised. + """ + if isinstance(signal, text_or_bytes): + signum = getattr(_signal, signal, None) + if signum is None: + raise ValueError('No such signal: %r' % signal) + signame = signal + else: + try: + signame = self.signals[signal] + except KeyError: + raise ValueError('No such signal: %r' % signal) + signum = signal + + prev = _signal.signal(signum, self._handle_signal) + self._previous_handlers[signum] = prev + + if listener is not None: + self.bus.log('Listening for %s.' % signame) + self.bus.subscribe(signame, listener) + + def _handle_signal(self, signum=None, frame=None): + """Python signal handler (self.set_handler subscribes it for you).""" + signame = self.signals[signum] + self.bus.log('Caught signal %s.' % signame) + self.bus.publish(signame) + + def handle_SIGHUP(self): + """Restart if daemonized, else exit.""" + if self._is_daemonized(): + self.bus.log('SIGHUP caught while daemonized. Restarting.') + self.bus.restart() + else: + # not daemonized (may be foreground or background) + self.bus.log('SIGHUP caught but not daemonized. Exiting.') + self.bus.exit() + + +try: + import pwd + import grp +except ImportError: + pwd, grp = None, None + + +class DropPrivileges(SimplePlugin): + + """Drop privileges. uid/gid arguments not available on Windows. + + Special thanks to `Gavin Baker + `_ + """ + + def __init__(self, bus, umask=None, uid=None, gid=None): + SimplePlugin.__init__(self, bus) + self.finalized = False + self.uid = uid + self.gid = gid + self.umask = umask + + @property + def uid(self): + """The uid under which to run. Availability: Unix.""" + return self._uid + + @uid.setter + def uid(self, val): + if val is not None: + if pwd is None: + self.bus.log('pwd module not available; ignoring uid.', + level=30) + val = None + elif isinstance(val, text_or_bytes): + val = pwd.getpwnam(val)[2] + self._uid = val + + @property + def gid(self): + """The gid under which to run. Availability: Unix.""" + return self._gid + + @gid.setter + def gid(self, val): + if val is not None: + if grp is None: + self.bus.log('grp module not available; ignoring gid.', + level=30) + val = None + elif isinstance(val, text_or_bytes): + val = grp.getgrnam(val)[2] + self._gid = val + + @property + def umask(self): + """The default permission mode for newly created files and directories. + + Usually expressed in octal format, for example, ``0644``. + Availability: Unix, Windows. + """ + return self._umask + + @umask.setter + def umask(self, val): + if val is not None: + try: + os.umask + except AttributeError: + self.bus.log('umask function not available; ignoring umask.', + level=30) + val = None + self._umask = val + + def start(self): + # uid/gid + def current_ids(): + """Return the current (uid, gid) if available.""" + name, group = None, None + if pwd: + name = pwd.getpwuid(os.getuid())[0] + if grp: + group = grp.getgrgid(os.getgid())[0] + return name, group + + if self.finalized: + if not (self.uid is None and self.gid is None): + self.bus.log('Already running as uid: %r gid: %r' % + current_ids()) + else: + if self.uid is None and self.gid is None: + if pwd or grp: + self.bus.log('uid/gid not set', level=30) + else: + self.bus.log('Started as uid: %r gid: %r' % current_ids()) + if self.gid is not None: + os.setgid(self.gid) + os.setgroups([]) + if self.uid is not None: + os.setuid(self.uid) + self.bus.log('Running as uid: %r gid: %r' % current_ids()) + + # umask + if self.finalized: + if self.umask is not None: + self.bus.log('umask already set to: %03o' % self.umask) + else: + if self.umask is None: + self.bus.log('umask not set', level=30) + else: + old_umask = os.umask(self.umask) + self.bus.log('umask old: %03o, new: %03o' % + (old_umask, self.umask)) + + self.finalized = True + # This is slightly higher than the priority for server.start + # in order to facilitate the most common use: starting on a low + # port (which requires root) and then dropping to another user. + start.priority = 77 + + +class Daemonizer(SimplePlugin): + + """Daemonize the running script. + + Use this with a Web Site Process Bus via:: + + Daemonizer(bus).subscribe() + + When this component finishes, the process is completely decoupled from + the parent environment. Please note that when this component is used, + the return code from the parent process will still be 0 if a startup + error occurs in the forked children. Errors in the initial daemonizing + process still return proper exit codes. Therefore, if you use this + plugin to daemonize, don't use the return code as an accurate indicator + of whether the process fully started. In fact, that return code only + indicates if the process successfully finished the first fork. + """ + + def __init__(self, bus, stdin='/dev/null', stdout='/dev/null', + stderr='/dev/null'): + SimplePlugin.__init__(self, bus) + self.stdin = stdin + self.stdout = stdout + self.stderr = stderr + self.finalized = False + + def start(self): + if self.finalized: + self.bus.log('Already deamonized.') + + # forking has issues with threads: + # http://www.opengroup.org/onlinepubs/000095399/functions/fork.html + # "The general problem with making fork() work in a multi-threaded + # world is what to do with all of the threads..." + # So we check for active threads: + if threading.activeCount() != 1: + self.bus.log('There are %r active threads. ' + 'Daemonizing now may cause strange failures.' % + threading.enumerate(), level=30) + + self.daemonize(self.stdin, self.stdout, self.stderr, self.bus.log) + + self.finalized = True + start.priority = 65 + + @staticmethod + def daemonize( + stdin='/dev/null', stdout='/dev/null', stderr='/dev/null', + logger=lambda msg: None): + # See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16 + # (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7) + # and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012 + + # Finish up with the current stdout/stderr + sys.stdout.flush() + sys.stderr.flush() + + error_tmpl = ( + '{sys.argv[0]}: fork #{n} failed: ({exc.errno}) {exc.strerror}\n' + ) + + for fork in range(2): + msg = ['Forking once.', 'Forking twice.'][fork] + try: + pid = os.fork() + if pid > 0: + # This is the parent; exit. + logger(msg) + os._exit(0) + except OSError as exc: + # Python raises OSError rather than returning negative numbers. + sys.exit(error_tmpl.format(sys=sys, exc=exc, n=fork + 1)) + if fork == 0: + os.setsid() + + os.umask(0) + + si = open(stdin, 'r') + so = open(stdout, 'a+') + se = open(stderr, 'a+') + + # os.dup2(fd, fd2) will close fd2 if necessary, + # so we don't explicitly close stdin/out/err. + # See http://docs.python.org/lib/os-fd-ops.html + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + logger('Daemonized to PID: %s' % os.getpid()) + + +class PIDFile(SimplePlugin): + + """Maintain a PID file via a WSPBus.""" + + def __init__(self, bus, pidfile): + SimplePlugin.__init__(self, bus) + self.pidfile = pidfile + self.finalized = False + + def start(self): + pid = os.getpid() + if self.finalized: + self.bus.log('PID %r already written to %r.' % (pid, self.pidfile)) + else: + open(self.pidfile, 'wb').write(ntob('%s\n' % pid, 'utf8')) + self.bus.log('PID %r written to %r.' % (pid, self.pidfile)) + self.finalized = True + start.priority = 70 + + def exit(self): + try: + os.remove(self.pidfile) + self.bus.log('PID file removed: %r.' % self.pidfile) + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + pass + + +class PerpetualTimer(Timer): + + """A responsive subclass of threading.Timer whose run() method repeats. + + Use this timer only when you really need a very interruptible timer; + this checks its 'finished' condition up to 20 times a second, which can + results in pretty high CPU usage + """ + + def __init__(self, *args, **kwargs): + "Override parent constructor to allow 'bus' to be provided." + self.bus = kwargs.pop('bus', None) + super(PerpetualTimer, self).__init__(*args, **kwargs) + + def run(self): + while True: + self.finished.wait(self.interval) + if self.finished.isSet(): + return + try: + self.function(*self.args, **self.kwargs) + except Exception: + if self.bus: + self.bus.log( + 'Error in perpetual timer thread function %r.' % + self.function, level=40, traceback=True) + # Quit on first error to avoid massive logs. + raise + + +class BackgroundTask(threading.Thread): + + """A subclass of threading.Thread whose run() method repeats. + + Use this class for most repeating tasks. It uses time.sleep() to wait + for each interval, which isn't very responsive; that is, even if you call + self.cancel(), you'll have to wait until the sleep() call finishes before + the thread stops. To compensate, it defaults to being daemonic, which means + it won't delay stopping the whole process. + """ + + def __init__(self, interval, function, args=[], kwargs={}, bus=None): + super(BackgroundTask, self).__init__() + self.interval = interval + self.function = function + self.args = args + self.kwargs = kwargs + self.running = False + self.bus = bus + + # default to daemonic + self.daemon = True + + def cancel(self): + self.running = False + + def run(self): + self.running = True + while self.running: + time.sleep(self.interval) + if not self.running: + return + try: + self.function(*self.args, **self.kwargs) + except Exception: + if self.bus: + self.bus.log('Error in background task thread function %r.' + % self.function, level=40, traceback=True) + # Quit on first error to avoid massive logs. + raise + + +class Monitor(SimplePlugin): + + """WSPBus listener to periodically run a callback in its own thread.""" + + callback = None + """The function to call at intervals.""" + + frequency = 60 + """The time in seconds between callback runs.""" + + thread = None + """A :class:`BackgroundTask` + thread. + """ + + def __init__(self, bus, callback, frequency=60, name=None): + SimplePlugin.__init__(self, bus) + self.callback = callback + self.frequency = frequency + self.thread = None + self.name = name + + def start(self): + """Start our callback in its own background thread.""" + if self.frequency > 0: + threadname = self.name or self.__class__.__name__ + if self.thread is None: + self.thread = BackgroundTask(self.frequency, self.callback, + bus=self.bus) + self.thread.setName(threadname) + self.thread.start() + self.bus.log('Started monitor thread %r.' % threadname) + else: + self.bus.log('Monitor thread %r already started.' % threadname) + start.priority = 70 + + def stop(self): + """Stop our callback's background task thread.""" + if self.thread is None: + self.bus.log('No thread running for %s.' % + self.name or self.__class__.__name__) + else: + if self.thread is not threading.currentThread(): + name = self.thread.getName() + self.thread.cancel() + if not self.thread.daemon: + self.bus.log('Joining %r' % name) + self.thread.join() + self.bus.log('Stopped thread %r.' % name) + self.thread = None + + def graceful(self): + """Stop the callback's background task thread and restart it.""" + self.stop() + self.start() + + +class Autoreloader(Monitor): + + """Monitor which re-executes the process when files change. + + This :ref:`plugin` restarts the process (via :func:`os.execv`) + if any of the files it monitors change (or is deleted). By default, the + autoreloader monitors all imported modules; you can add to the + set by adding to ``autoreload.files``:: + + cherrypy.engine.autoreload.files.add(myFile) + + If there are imported files you do *not* wish to monitor, you can + adjust the ``match`` attribute, a regular expression. For example, + to stop monitoring cherrypy itself:: + + cherrypy.engine.autoreload.match = r'^(?!cherrypy).+' + + Like all :class:`Monitor` plugins, + the autoreload plugin takes a ``frequency`` argument. The default is + 1 second; that is, the autoreloader will examine files once each second. + """ + + files = None + """The set of files to poll for modifications.""" + + frequency = 1 + """The interval in seconds at which to poll for modified files.""" + + match = '.*' + """A regular expression by which to match filenames.""" + + def __init__(self, bus, frequency=1, match='.*'): + self.mtimes = {} + self.files = set() + self.match = match + Monitor.__init__(self, bus, self.run, frequency) + + def start(self): + """Start our own background task thread for self.run.""" + if self.thread is None: + self.mtimes = {} + Monitor.start(self) + start.priority = 70 + + def sysfiles(self): + """Return a Set of sys.modules filenames to monitor.""" + search_mod_names = filter(re.compile(self.match).match, sys.modules) + mods = map(sys.modules.get, search_mod_names) + return set(filter(None, map(self._file_for_module, mods))) + + @classmethod + def _file_for_module(cls, module): + """Return the relevant file for the module.""" + return ( + cls._archive_for_zip_module(module) + or cls._file_for_file_module(module) + ) + + @staticmethod + def _archive_for_zip_module(module): + """Return the archive filename for the module if relevant.""" + try: + return module.__loader__.archive + except AttributeError: + pass + + @classmethod + def _file_for_file_module(cls, module): + """Return the file for the module.""" + try: + return module.__file__ and cls._make_absolute(module.__file__) + except AttributeError: + pass + + @staticmethod + def _make_absolute(filename): + """Ensure filename is absolute to avoid effect of os.chdir.""" + return filename if os.path.isabs(filename) else ( + os.path.normpath(os.path.join(_module__file__base, filename)) + ) + + def run(self): + """Reload the process if registered files have been modified.""" + for filename in self.sysfiles() | self.files: + if filename: + if filename.endswith('.pyc'): + filename = filename[:-1] + + oldtime = self.mtimes.get(filename, 0) + if oldtime is None: + # Module with no .py file. Skip it. + continue + + try: + mtime = os.stat(filename).st_mtime + except OSError: + # Either a module with no .py file, or it's been deleted. + mtime = None + + if filename not in self.mtimes: + # If a module has no .py file, this will be None. + self.mtimes[filename] = mtime + else: + if mtime is None or mtime > oldtime: + # The file has been deleted or modified. + self.bus.log('Restarting because %s changed.' % + filename) + self.thread.cancel() + self.bus.log('Stopped thread %r.' % + self.thread.getName()) + self.bus.restart() + return + + +class ThreadManager(SimplePlugin): + + """Manager for HTTP request threads. + + If you have control over thread creation and destruction, publish to + the 'acquire_thread' and 'release_thread' channels (for each thread). + This will register/unregister the current thread and publish to + 'start_thread' and 'stop_thread' listeners in the bus as needed. + + If threads are created and destroyed by code you do not control + (e.g., Apache), then, at the beginning of every HTTP request, + publish to 'acquire_thread' only. You should not publish to + 'release_thread' in this case, since you do not know whether + the thread will be re-used or not. The bus will call + 'stop_thread' listeners for you when it stops. + """ + + threads = None + """A map of {thread ident: index number} pairs.""" + + def __init__(self, bus): + self.threads = {} + SimplePlugin.__init__(self, bus) + self.bus.listeners.setdefault('acquire_thread', set()) + self.bus.listeners.setdefault('start_thread', set()) + self.bus.listeners.setdefault('release_thread', set()) + self.bus.listeners.setdefault('stop_thread', set()) + + def acquire_thread(self): + """Run 'start_thread' listeners for the current thread. + + If the current thread has already been seen, any 'start_thread' + listeners will not be run again. + """ + thread_ident = _thread.get_ident() + if thread_ident not in self.threads: + # We can't just use get_ident as the thread ID + # because some platforms reuse thread ID's. + i = len(self.threads) + 1 + self.threads[thread_ident] = i + self.bus.publish('start_thread', i) + + def release_thread(self): + """Release the current thread and run 'stop_thread' listeners.""" + thread_ident = _thread.get_ident() + i = self.threads.pop(thread_ident, None) + if i is not None: + self.bus.publish('stop_thread', i) + + def stop(self): + """Release all threads and run all 'stop_thread' listeners.""" + for thread_ident, i in self.threads.items(): + self.bus.publish('stop_thread', i) + self.threads.clear() + graceful = stop diff --git a/resources/lib/cherrypy/process/servers.py b/resources/lib/cherrypy/process/servers.py new file mode 100644 index 0000000..dcb34de --- /dev/null +++ b/resources/lib/cherrypy/process/servers.py @@ -0,0 +1,416 @@ +r""" +Starting in CherryPy 3.1, cherrypy.server is implemented as an +:ref:`Engine Plugin`. It's an instance of +:class:`cherrypy._cpserver.Server`, which is a subclass of +:class:`cherrypy.process.servers.ServerAdapter`. The ``ServerAdapter`` class +is designed to control other servers, as well. + +Multiple servers/ports +====================== + +If you need to start more than one HTTP server (to serve on multiple ports, or +protocols, etc.), you can manually register each one and then start them all +with engine.start:: + + s1 = ServerAdapter( + cherrypy.engine, + MyWSGIServer(host='0.0.0.0', port=80) + ) + s2 = ServerAdapter( + cherrypy.engine, + another.HTTPServer(host='127.0.0.1', SSL=True) + ) + s1.subscribe() + s2.subscribe() + cherrypy.engine.start() + +.. index:: SCGI + +FastCGI/SCGI +============ + +There are also Flup\ **F**\ CGIServer and Flup\ **S**\ CGIServer classes in +:mod:`cherrypy.process.servers`. To start an fcgi server, for example, +wrap an instance of it in a ServerAdapter:: + + addr = ('0.0.0.0', 4000) + f = servers.FlupFCGIServer(application=cherrypy.tree, bindAddress=addr) + s = servers.ServerAdapter(cherrypy.engine, httpserver=f, bind_addr=addr) + s.subscribe() + +The :doc:`cherryd` startup script will do the above for +you via its `-f` flag. +Note that you need to download and install `flup `_ +yourself, whether you use ``cherryd`` or not. + +.. _fastcgi: +.. index:: FastCGI + +FastCGI +------- + +A very simple setup lets your cherry run with FastCGI. +You just need the flup library, +plus a running Apache server (with ``mod_fastcgi``) or lighttpd server. + +CherryPy code +^^^^^^^^^^^^^ + +hello.py:: + + #!/usr/bin/python + import cherrypy + + class HelloWorld: + '''Sample request handler class.''' + @cherrypy.expose + def index(self): + return "Hello world!" + + cherrypy.tree.mount(HelloWorld()) + # CherryPy autoreload must be disabled for the flup server to work + cherrypy.config.update({'engine.autoreload.on':False}) + +Then run :doc:`/deployguide/cherryd` with the '-f' arg:: + + cherryd -c -d -f -i hello.py + +Apache +^^^^^^ + +At the top level in httpd.conf:: + + FastCgiIpcDir /tmp + FastCgiServer /path/to/cherry.fcgi -idle-timeout 120 -processes 4 + +And inside the relevant VirtualHost section:: + + # FastCGI config + AddHandler fastcgi-script .fcgi + ScriptAliasMatch (.*$) /path/to/cherry.fcgi$1 + +Lighttpd +^^^^^^^^ + +For `Lighttpd `_ you can follow these +instructions. Within ``lighttpd.conf`` make sure ``mod_fastcgi`` is +active within ``server.modules``. Then, within your ``$HTTP["host"]`` +directive, configure your fastcgi script like the following:: + + $HTTP["url"] =~ "" { + fastcgi.server = ( + "/" => ( + "script.fcgi" => ( + "bin-path" => "/path/to/your/script.fcgi", + "socket" => "/tmp/script.sock", + "check-local" => "disable", + "disable-time" => 1, + "min-procs" => 1, + "max-procs" => 1, # adjust as needed + ), + ), + ) + } # end of $HTTP["url"] =~ "^/" + +Please see `Lighttpd FastCGI Docs +`_ for +an explanation of the possible configuration options. +""" + +import os +import sys +import time +import warnings +import contextlib + +import portend + + +class Timeouts: + occupied = 5 + free = 1 + + +class ServerAdapter(object): + + """Adapter for an HTTP server. + + If you need to start more than one HTTP server (to serve on multiple + ports, or protocols, etc.), you can manually register each one and then + start them all with bus.start:: + + s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80)) + s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True)) + s1.subscribe() + s2.subscribe() + bus.start() + """ + + def __init__(self, bus, httpserver=None, bind_addr=None): + self.bus = bus + self.httpserver = httpserver + self.bind_addr = bind_addr + self.interrupt = None + self.running = False + + def subscribe(self): + self.bus.subscribe('start', self.start) + self.bus.subscribe('stop', self.stop) + + def unsubscribe(self): + self.bus.unsubscribe('start', self.start) + self.bus.unsubscribe('stop', self.stop) + + def start(self): + """Start the HTTP server.""" + if self.running: + self.bus.log('Already serving on %s' % self.description) + return + + self.interrupt = None + if not self.httpserver: + raise ValueError('No HTTP server has been created.') + + if not os.environ.get('LISTEN_PID', None): + # Start the httpserver in a new thread. + if isinstance(self.bind_addr, tuple): + portend.free(*self.bind_addr, timeout=Timeouts.free) + + import threading + t = threading.Thread(target=self._start_http_thread) + t.setName('HTTPServer ' + t.getName()) + t.start() + + self.wait() + self.running = True + self.bus.log('Serving on %s' % self.description) + start.priority = 75 + + @property + def description(self): + """ + A description about where this server is bound. + """ + if self.bind_addr is None: + on_what = 'unknown interface (dynamic?)' + elif isinstance(self.bind_addr, tuple): + on_what = self._get_base() + else: + on_what = 'socket file: %s' % self.bind_addr + return on_what + + def _get_base(self): + if not self.httpserver: + return '' + host, port = self.bound_addr + if getattr(self.httpserver, 'ssl_adapter', None): + scheme = 'https' + if port != 443: + host += ':%s' % port + else: + scheme = 'http' + if port != 80: + host += ':%s' % port + + return '%s://%s' % (scheme, host) + + def _start_http_thread(self): + """HTTP servers MUST be running in new threads, so that the + main thread persists to receive KeyboardInterrupt's. If an + exception is raised in the httpserver's thread then it's + trapped here, and the bus (and therefore our httpserver) + are shut down. + """ + try: + self.httpserver.start() + except KeyboardInterrupt: + self.bus.log(' hit: shutting down HTTP server') + self.interrupt = sys.exc_info()[1] + self.bus.exit() + except SystemExit: + self.bus.log('SystemExit raised: shutting down HTTP server') + self.interrupt = sys.exc_info()[1] + self.bus.exit() + raise + except Exception: + self.interrupt = sys.exc_info()[1] + self.bus.log('Error in HTTP server: shutting down', + traceback=True, level=40) + self.bus.exit() + raise + + def wait(self): + """Wait until the HTTP server is ready to receive requests.""" + while not getattr(self.httpserver, 'ready', False): + if self.interrupt: + raise self.interrupt + time.sleep(.1) + + # bypass check when LISTEN_PID is set + if os.environ.get('LISTEN_PID', None): + return + + # bypass check when running via socket-activation + # (for socket-activation the port will be managed by systemd) + if not isinstance(self.bind_addr, tuple): + return + + # wait for port to be occupied + with _safe_wait(*self.bound_addr): + portend.occupied(*self.bound_addr, timeout=Timeouts.occupied) + + @property + def bound_addr(self): + """ + The bind address, or if it's an ephemeral port and the + socket has been bound, return the actual port bound. + """ + host, port = self.bind_addr + if port == 0 and self.httpserver.socket: + # Bound to ephemeral port. Get the actual port allocated. + port = self.httpserver.socket.getsockname()[1] + return host, port + + def stop(self): + """Stop the HTTP server.""" + if self.running: + # stop() MUST block until the server is *truly* stopped. + self.httpserver.stop() + # Wait for the socket to be truly freed. + if isinstance(self.bind_addr, tuple): + portend.free(*self.bound_addr, timeout=Timeouts.free) + self.running = False + self.bus.log('HTTP Server %s shut down' % self.httpserver) + else: + self.bus.log('HTTP Server %s already shut down' % self.httpserver) + stop.priority = 25 + + def restart(self): + """Restart the HTTP server.""" + self.stop() + self.start() + + +class FlupCGIServer(object): + + """Adapter for a flup.server.cgi.WSGIServer.""" + + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + self.ready = False + + def start(self): + """Start the CGI server.""" + # We have to instantiate the server class here because its __init__ + # starts a threadpool. If we do it too early, daemonize won't work. + from flup.server.cgi import WSGIServer + + self.cgiserver = WSGIServer(*self.args, **self.kwargs) + self.ready = True + self.cgiserver.run() + + def stop(self): + """Stop the HTTP server.""" + self.ready = False + + +class FlupFCGIServer(object): + + """Adapter for a flup.server.fcgi.WSGIServer.""" + + def __init__(self, *args, **kwargs): + if kwargs.get('bindAddress', None) is None: + import socket + if not hasattr(socket, 'fromfd'): + raise ValueError( + 'Dynamic FCGI server not available on this platform. ' + 'You must use a static or external one by providing a ' + 'legal bindAddress.') + self.args = args + self.kwargs = kwargs + self.ready = False + + def start(self): + """Start the FCGI server.""" + # We have to instantiate the server class here because its __init__ + # starts a threadpool. If we do it too early, daemonize won't work. + from flup.server.fcgi import WSGIServer + self.fcgiserver = WSGIServer(*self.args, **self.kwargs) + # TODO: report this bug upstream to flup. + # If we don't set _oldSIGs on Windows, we get: + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 108, in run + # self._restoreSignalHandlers() + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 156, in _restoreSignalHandlers + # for signum,handler in self._oldSIGs: + # AttributeError: 'WSGIServer' object has no attribute '_oldSIGs' + self.fcgiserver._installSignalHandlers = lambda: None + self.fcgiserver._oldSIGs = [] + self.ready = True + self.fcgiserver.run() + + def stop(self): + """Stop the HTTP server.""" + # Forcibly stop the fcgi server main event loop. + self.fcgiserver._keepGoing = False + # Force all worker threads to die off. + self.fcgiserver._threadPool.maxSpare = ( + self.fcgiserver._threadPool._idleCount) + self.ready = False + + +class FlupSCGIServer(object): + + """Adapter for a flup.server.scgi.WSGIServer.""" + + def __init__(self, *args, **kwargs): + self.args = args + self.kwargs = kwargs + self.ready = False + + def start(self): + """Start the SCGI server.""" + # We have to instantiate the server class here because its __init__ + # starts a threadpool. If we do it too early, daemonize won't work. + from flup.server.scgi import WSGIServer + self.scgiserver = WSGIServer(*self.args, **self.kwargs) + # TODO: report this bug upstream to flup. + # If we don't set _oldSIGs on Windows, we get: + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 108, in run + # self._restoreSignalHandlers() + # File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py", + # line 156, in _restoreSignalHandlers + # for signum,handler in self._oldSIGs: + # AttributeError: 'WSGIServer' object has no attribute '_oldSIGs' + self.scgiserver._installSignalHandlers = lambda: None + self.scgiserver._oldSIGs = [] + self.ready = True + self.scgiserver.run() + + def stop(self): + """Stop the HTTP server.""" + self.ready = False + # Forcibly stop the scgi server main event loop. + self.scgiserver._keepGoing = False + # Force all worker threads to die off. + self.scgiserver._threadPool.maxSpare = 0 + + +@contextlib.contextmanager +def _safe_wait(host, port): + """ + On systems where a loopback interface is not available and the + server is bound to all interfaces, it's difficult to determine + whether the server is in fact occupying the port. In this case, + just issue a warning and move on. See issue #1100. + """ + try: + yield + except portend.Timeout: + if host == portend.client_host(host): + raise + msg = 'Unable to verify that the server is bound on %r' % port + warnings.warn(msg) diff --git a/resources/lib/cherrypy/process/win32.py b/resources/lib/cherrypy/process/win32.py new file mode 100644 index 0000000..096b027 --- /dev/null +++ b/resources/lib/cherrypy/process/win32.py @@ -0,0 +1,183 @@ +"""Windows service. Requires pywin32.""" + +import os +import win32api +import win32con +import win32event +import win32service +import win32serviceutil + +from cherrypy.process import wspbus, plugins + + +class ConsoleCtrlHandler(plugins.SimplePlugin): + + """A WSPBus plugin for handling Win32 console events (like Ctrl-C).""" + + def __init__(self, bus): + self.is_set = False + plugins.SimplePlugin.__init__(self, bus) + + def start(self): + if self.is_set: + self.bus.log('Handler for console events already set.', level=40) + return + + result = win32api.SetConsoleCtrlHandler(self.handle, 1) + if result == 0: + self.bus.log('Could not SetConsoleCtrlHandler (error %r)' % + win32api.GetLastError(), level=40) + else: + self.bus.log('Set handler for console events.', level=40) + self.is_set = True + + def stop(self): + if not self.is_set: + self.bus.log('Handler for console events already off.', level=40) + return + + try: + result = win32api.SetConsoleCtrlHandler(self.handle, 0) + except ValueError: + # "ValueError: The object has not been registered" + result = 1 + + if result == 0: + self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' % + win32api.GetLastError(), level=40) + else: + self.bus.log('Removed handler for console events.', level=40) + self.is_set = False + + def handle(self, event): + """Handle console control events (like Ctrl-C).""" + if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT, + win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT, + win32con.CTRL_CLOSE_EVENT): + self.bus.log('Console event %s: shutting down bus' % event) + + # Remove self immediately so repeated Ctrl-C doesn't re-call it. + try: + self.stop() + except ValueError: + pass + + self.bus.exit() + # 'First to return True stops the calls' + return 1 + return 0 + + +class Win32Bus(wspbus.Bus): + + """A Web Site Process Bus implementation for Win32. + + Instead of time.sleep, this bus blocks using native win32event objects. + """ + + def __init__(self): + self.events = {} + wspbus.Bus.__init__(self) + + def _get_state_event(self, state): + """Return a win32event for the given state (creating it if needed).""" + try: + return self.events[state] + except KeyError: + event = win32event.CreateEvent(None, 0, 0, + 'WSPBus %s Event (pid=%r)' % + (state.name, os.getpid())) + self.events[state] = event + return event + + @property + def state(self): + return self._state + + @state.setter + def state(self, value): + self._state = value + event = self._get_state_event(value) + win32event.PulseEvent(event) + + def wait(self, state, interval=0.1, channel=None): + """Wait for the given state(s), KeyboardInterrupt or SystemExit. + + Since this class uses native win32event objects, the interval + argument is ignored. + """ + if isinstance(state, (tuple, list)): + # Don't wait for an event that beat us to the punch ;) + if self.state not in state: + events = tuple([self._get_state_event(s) for s in state]) + win32event.WaitForMultipleObjects( + events, 0, win32event.INFINITE) + else: + # Don't wait for an event that beat us to the punch ;) + if self.state != state: + event = self._get_state_event(state) + win32event.WaitForSingleObject(event, win32event.INFINITE) + + +class _ControlCodes(dict): + + """Control codes used to "signal" a service via ControlService. + + User-defined control codes are in the range 128-255. We generally use + the standard Python value for the Linux signal and add 128. Example: + + >>> signal.SIGUSR1 + 10 + control_codes['graceful'] = 128 + 10 + """ + + def key_for(self, obj): + """For the given value, return its corresponding key.""" + for key, val in self.items(): + if val is obj: + return key + raise ValueError('The given object could not be found: %r' % obj) + + +control_codes = _ControlCodes({'graceful': 138}) + + +def signal_child(service, command): + if command == 'stop': + win32serviceutil.StopService(service) + elif command == 'restart': + win32serviceutil.RestartService(service) + else: + win32serviceutil.ControlService(service, control_codes[command]) + + +class PyWebService(win32serviceutil.ServiceFramework): + + """Python Web Service.""" + + _svc_name_ = 'Python Web Service' + _svc_display_name_ = 'Python Web Service' + _svc_deps_ = None # sequence of service names on which this depends + _exe_name_ = 'pywebsvc' + _exe_args_ = None # Default to no arguments + + # Only exists on Windows 2000 or later, ignored on windows NT + _svc_description_ = 'Python Web Service' + + def SvcDoRun(self): + from cherrypy import process + process.bus.start() + process.bus.block() + + def SvcStop(self): + from cherrypy import process + self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING) + process.bus.exit() + + def SvcOther(self, control): + from cherrypy import process + process.bus.publish(control_codes.key_for(control)) + + +if __name__ == '__main__': + win32serviceutil.HandleCommandLine(PyWebService) diff --git a/resources/lib/cherrypy/process/wspbus.py b/resources/lib/cherrypy/process/wspbus.py new file mode 100644 index 0000000..d91dba4 --- /dev/null +++ b/resources/lib/cherrypy/process/wspbus.py @@ -0,0 +1,590 @@ +r"""An implementation of the Web Site Process Bus. + +This module is completely standalone, depending only on the stdlib. + +Web Site Process Bus +-------------------- + +A Bus object is used to contain and manage site-wide behavior: +daemonization, HTTP server start/stop, process reload, signal handling, +drop privileges, PID file management, logging for all of these, +and many more. + +In addition, a Bus object provides a place for each web framework +to register code that runs in response to site-wide events (like +process start and stop), or which controls or otherwise interacts with +the site-wide components mentioned above. For example, a framework which +uses file-based templates would add known template filenames to an +autoreload component. + +Ideally, a Bus object will be flexible enough to be useful in a variety +of invocation scenarios: + + 1. The deployer starts a site from the command line via a + framework-neutral deployment script; applications from multiple frameworks + are mixed in a single site. Command-line arguments and configuration + files are used to define site-wide components such as the HTTP server, + WSGI component graph, autoreload behavior, signal handling, etc. + 2. The deployer starts a site via some other process, such as Apache; + applications from multiple frameworks are mixed in a single site. + Autoreload and signal handling (from Python at least) are disabled. + 3. The deployer starts a site via a framework-specific mechanism; + for example, when running tests, exploring tutorials, or deploying + single applications from a single framework. The framework controls + which site-wide components are enabled as it sees fit. + +The Bus object in this package uses topic-based publish-subscribe +messaging to accomplish all this. A few topic channels are built in +('start', 'stop', 'exit', 'graceful', 'log', and 'main'). Frameworks and +site containers are free to define their own. If a message is sent to a +channel that has not been defined or has no listeners, there is no effect. + +In general, there should only ever be a single Bus object per process. +Frameworks and site containers share a single Bus object by publishing +messages and subscribing listeners. + +The Bus object works as a finite state machine which models the current +state of the process. Bus methods move it from one state to another; +those methods then publish to subscribed listeners on the channel for +the new state.:: + + O + | + V + STOPPING --> STOPPED --> EXITING -> X + A A | + | \___ | + | \ | + | V V + STARTED <-- STARTING + +""" + +import atexit + +try: + import ctypes +except ImportError: + """Google AppEngine is shipped without ctypes + + :seealso: http://stackoverflow.com/a/6523777/70170 + """ + ctypes = None + +import operator +import os +import sys +import threading +import time +import traceback as _traceback +import warnings +import subprocess +import functools + +import six + + +# Here I save the value of os.getcwd(), which, if I am imported early enough, +# will be the directory from which the startup script was run. This is needed +# by _do_execv(), to change back to the original directory before execv()ing a +# new process. This is a defense against the application having changed the +# current working directory (which could make sys.executable "not found" if +# sys.executable is a relative-path, and/or cause other problems). +_startup_cwd = os.getcwd() + + +class ChannelFailures(Exception): + """Exception raised during errors on Bus.publish().""" + + delimiter = '\n' + + def __init__(self, *args, **kwargs): + """Initialize ChannelFailures errors wrapper.""" + super(ChannelFailures, self).__init__(*args, **kwargs) + self._exceptions = list() + + def handle_exception(self): + """Append the current exception to self.""" + self._exceptions.append(sys.exc_info()[1]) + + def get_instances(self): + """Return a list of seen exception instances.""" + return self._exceptions[:] + + def __str__(self): + """Render the list of errors, which happened in channel.""" + exception_strings = map(repr, self.get_instances()) + return self.delimiter.join(exception_strings) + + __repr__ = __str__ + + def __bool__(self): + """Determine whether any error happened in channel.""" + return bool(self._exceptions) + __nonzero__ = __bool__ + +# Use a flag to indicate the state of the bus. + + +class _StateEnum(object): + + class State(object): + name = None + + def __repr__(self): + return 'states.%s' % self.name + + def __setattr__(self, key, value): + if isinstance(value, self.State): + value.name = key + object.__setattr__(self, key, value) + + +states = _StateEnum() +states.STOPPED = states.State() +states.STARTING = states.State() +states.STARTED = states.State() +states.STOPPING = states.State() +states.EXITING = states.State() + + +try: + import fcntl +except ImportError: + max_files = 0 +else: + try: + max_files = os.sysconf('SC_OPEN_MAX') + except AttributeError: + max_files = 1024 + + +class Bus(object): + """Process state-machine and messenger for HTTP site deployment. + + All listeners for a given channel are guaranteed to be called even + if others at the same channel fail. Each failure is logged, but + execution proceeds on to the next listener. The only way to stop all + processing from inside a listener is to raise SystemExit and stop the + whole server. + """ + + states = states + state = states.STOPPED + execv = False + max_cloexec_files = max_files + + def __init__(self): + """Initialize pub/sub bus.""" + self.execv = False + self.state = states.STOPPED + channels = 'start', 'stop', 'exit', 'graceful', 'log', 'main' + self.listeners = dict( + (channel, set()) + for channel in channels + ) + self._priorities = {} + + def subscribe(self, channel, callback=None, priority=None): + """Add the given callback at the given channel (if not present). + + If callback is None, return a partial suitable for decorating + the callback. + """ + if callback is None: + return functools.partial( + self.subscribe, + channel, + priority=priority, + ) + + ch_listeners = self.listeners.setdefault(channel, set()) + ch_listeners.add(callback) + + if priority is None: + priority = getattr(callback, 'priority', 50) + self._priorities[(channel, callback)] = priority + + def unsubscribe(self, channel, callback): + """Discard the given callback (if present).""" + listeners = self.listeners.get(channel) + if listeners and callback in listeners: + listeners.discard(callback) + del self._priorities[(channel, callback)] + + def publish(self, channel, *args, **kwargs): + """Return output of all subscribers for the given channel.""" + if channel not in self.listeners: + return [] + + exc = ChannelFailures() + output = [] + + raw_items = ( + (self._priorities[(channel, listener)], listener) + for listener in self.listeners[channel] + ) + items = sorted(raw_items, key=operator.itemgetter(0)) + for priority, listener in items: + try: + output.append(listener(*args, **kwargs)) + except KeyboardInterrupt: + raise + except SystemExit: + e = sys.exc_info()[1] + # If we have previous errors ensure the exit code is non-zero + if exc and e.code == 0: + e.code = 1 + raise + except Exception: + exc.handle_exception() + if channel == 'log': + # Assume any further messages to 'log' will fail. + pass + else: + self.log('Error in %r listener %r' % (channel, listener), + level=40, traceback=True) + if exc: + raise exc + return output + + def _clean_exit(self): + """Assert that the Bus is not running in atexit handler callback.""" + if self.state != states.EXITING: + warnings.warn( + 'The main thread is exiting, but the Bus is in the %r state; ' + 'shutting it down automatically now. You must either call ' + 'bus.block() after start(), or call bus.exit() before the ' + 'main thread exits.' % self.state, RuntimeWarning) + self.exit() + + def start(self): + """Start all services.""" + atexit.register(self._clean_exit) + + self.state = states.STARTING + self.log('Bus STARTING') + try: + self.publish('start') + self.state = states.STARTED + self.log('Bus STARTED') + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + self.log('Shutting down due to error in start listener:', + level=40, traceback=True) + e_info = sys.exc_info()[1] + try: + self.exit() + except Exception: + # Any stop/exit errors will be logged inside publish(). + pass + # Re-raise the original error + raise e_info + + def exit(self): + """Stop all services and prepare to exit the process.""" + exitstate = self.state + EX_SOFTWARE = 70 + try: + self.stop() + + self.state = states.EXITING + self.log('Bus EXITING') + self.publish('exit') + # This isn't strictly necessary, but it's better than seeing + # "Waiting for child threads to terminate..." and then nothing. + self.log('Bus EXITED') + except Exception: + # This method is often called asynchronously (whether thread, + # signal handler, console handler, or atexit handler), so we + # can't just let exceptions propagate out unhandled. + # Assume it's been logged and just die. + os._exit(EX_SOFTWARE) + + if exitstate == states.STARTING: + # exit() was called before start() finished, possibly due to + # Ctrl-C because a start listener got stuck. In this case, + # we could get stuck in a loop where Ctrl-C never exits the + # process, so we just call os.exit here. + os._exit(EX_SOFTWARE) + + def restart(self): + """Restart the process (may close connections). + + This method does not restart the process from the calling thread; + instead, it stops the bus and asks the main thread to call execv. + """ + self.execv = True + self.exit() + + def graceful(self): + """Advise all services to reload.""" + self.log('Bus graceful') + self.publish('graceful') + + def block(self, interval=0.1): + """Wait for the EXITING state, KeyboardInterrupt or SystemExit. + + This function is intended to be called only by the main thread. + After waiting for the EXITING state, it also waits for all threads + to terminate, and then calls os.execv if self.execv is True. This + design allows another thread to call bus.restart, yet have the main + thread perform the actual execv call (required on some platforms). + """ + try: + self.wait(states.EXITING, interval=interval, channel='main') + except (KeyboardInterrupt, IOError): + # The time.sleep call might raise + # "IOError: [Errno 4] Interrupted function call" on KBInt. + self.log('Keyboard Interrupt: shutting down bus') + self.exit() + except SystemExit: + self.log('SystemExit raised: shutting down bus') + self.exit() + raise + + # Waiting for ALL child threads to finish is necessary on OS X. + # See https://github.com/cherrypy/cherrypy/issues/581. + # It's also good to let them all shut down before allowing + # the main thread to call atexit handlers. + # See https://github.com/cherrypy/cherrypy/issues/751. + self.log('Waiting for child threads to terminate...') + for t in threading.enumerate(): + # Validate the we're not trying to join the MainThread + # that will cause a deadlock and the case exist when + # implemented as a windows service and in any other case + # that another thread executes cherrypy.engine.exit() + if ( + t != threading.currentThread() and + not isinstance(t, threading._MainThread) and + # Note that any dummy (external) threads are + # always daemonic. + not t.daemon + ): + self.log('Waiting for thread %s.' % t.getName()) + t.join() + + if self.execv: + self._do_execv() + + def wait(self, state, interval=0.1, channel=None): + """Poll for the given state(s) at intervals; publish to channel.""" + if isinstance(state, (tuple, list)): + states = state + else: + states = [state] + + while self.state not in states: + time.sleep(interval) + self.publish(channel) + + def _do_execv(self): + """Re-execute the current process. + + This must be called from the main thread, because certain platforms + (OS X) don't allow execv to be called in a child thread very well. + """ + try: + args = self._get_true_argv() + except NotImplementedError: + """It's probably win32 or GAE""" + args = [sys.executable] + self._get_interpreter_argv() + sys.argv + + self.log('Re-spawning %s' % ' '.join(args)) + + self._extend_pythonpath(os.environ) + + if sys.platform[:4] == 'java': + from _systemrestart import SystemRestart + raise SystemRestart + else: + if sys.platform == 'win32': + args = ['"%s"' % arg for arg in args] + + os.chdir(_startup_cwd) + if self.max_cloexec_files: + self._set_cloexec() + os.execv(sys.executable, args) + + @staticmethod + def _get_interpreter_argv(): + """Retrieve current Python interpreter's arguments. + + Returns empty tuple in case of frozen mode, uses built-in arguments + reproduction function otherwise. + + Frozen mode is possible for the app has been packaged into a binary + executable using py2exe. In this case the interpreter's arguments are + already built-in into that executable. + + :seealso: https://github.com/cherrypy/cherrypy/issues/1526 + Ref: https://pythonhosted.org/PyInstaller/runtime-information.html + """ + return ([] + if getattr(sys, 'frozen', False) + else subprocess._args_from_interpreter_flags()) + + @staticmethod + def _get_true_argv(): + """Retrieve all real arguments of the python interpreter. + + ...even those not listed in ``sys.argv`` + + :seealso: http://stackoverflow.com/a/28338254/595220 + :seealso: http://stackoverflow.com/a/6683222/595220 + :seealso: http://stackoverflow.com/a/28414807/595220 + """ + try: + char_p = ctypes.c_char_p if six.PY2 else ctypes.c_wchar_p + + argv = ctypes.POINTER(char_p)() + argc = ctypes.c_int() + + ctypes.pythonapi.Py_GetArgcArgv( + ctypes.byref(argc), + ctypes.byref(argv), + ) + + _argv = argv[:argc.value] + + # The code below is trying to correctly handle special cases. + # `-c`'s argument interpreted by Python itself becomes `-c` as + # well. Same applies to `-m`. This snippet is trying to survive + # at least the case with `-m` + # Ref: https://github.com/cherrypy/cherrypy/issues/1545 + # Ref: python/cpython@418baf9 + argv_len, is_command, is_module = len(_argv), False, False + + try: + m_ind = _argv.index('-m') + if m_ind < argv_len - 1 and _argv[m_ind + 1] in ('-c', '-m'): + """ + In some older Python versions `-m`'s argument may be + substituted with `-c`, not `-m` + """ + is_module = True + except (IndexError, ValueError): + m_ind = None + + try: + c_ind = _argv.index('-c') + if c_ind < argv_len - 1 and _argv[c_ind + 1] == '-c': + is_command = True + except (IndexError, ValueError): + c_ind = None + + if is_module: + """It's containing `-m -m` sequence of arguments""" + if is_command and c_ind < m_ind: + """There's `-c -c` before `-m`""" + raise RuntimeError( + "Cannot reconstruct command from '-c'. Ref: " + 'https://github.com/cherrypy/cherrypy/issues/1545') + # Survive module argument here + original_module = sys.argv[0] + if not os.access(original_module, os.R_OK): + """There's no such module exist""" + raise AttributeError( + "{} doesn't seem to be a module " + 'accessible by current user'.format(original_module)) + del _argv[m_ind:m_ind + 2] # remove `-m -m` + # ... and substitute it with the original module path: + _argv.insert(m_ind, original_module) + elif is_command: + """It's containing just `-c -c` sequence of arguments""" + raise RuntimeError( + "Cannot reconstruct command from '-c'. " + 'Ref: https://github.com/cherrypy/cherrypy/issues/1545') + except AttributeError: + """It looks Py_GetArgcArgv is completely absent in some environments + + It is known, that there's no Py_GetArgcArgv in MS Windows and + ``ctypes`` module is completely absent in Google AppEngine + + :seealso: https://github.com/cherrypy/cherrypy/issues/1506 + :seealso: https://github.com/cherrypy/cherrypy/issues/1512 + :ref: http://bit.ly/2gK6bXK + """ + raise NotImplementedError + else: + return _argv + + @staticmethod + def _extend_pythonpath(env): + """Prepend current working dir to PATH environment variable if needed. + + If sys.path[0] is an empty string, the interpreter was likely + invoked with -m and the effective path is about to change on + re-exec. Add the current directory to $PYTHONPATH to ensure + that the new process sees the same path. + + This issue cannot be addressed in the general case because + Python cannot reliably reconstruct the + original command line (http://bugs.python.org/issue14208). + + (This idea filched from tornado.autoreload) + """ + path_prefix = '.' + os.pathsep + existing_path = env.get('PYTHONPATH', '') + needs_patch = ( + sys.path[0] == '' and + not existing_path.startswith(path_prefix) + ) + + if needs_patch: + env['PYTHONPATH'] = path_prefix + existing_path + + def _set_cloexec(self): + """Set the CLOEXEC flag on all open files (except stdin/out/err). + + If self.max_cloexec_files is an integer (the default), then on + platforms which support it, it represents the max open files setting + for the operating system. This function will be called just before + the process is restarted via os.execv() to prevent open files + from persisting into the new process. + + Set self.max_cloexec_files to 0 to disable this behavior. + """ + for fd in range(3, self.max_cloexec_files): # skip stdin/out/err + try: + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + except IOError: + continue + fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC) + + def stop(self): + """Stop all services.""" + self.state = states.STOPPING + self.log('Bus STOPPING') + self.publish('stop') + self.state = states.STOPPED + self.log('Bus STOPPED') + + def start_with_callback(self, func, args=None, kwargs=None): + """Start 'func' in a new thread T, then start self (and return T).""" + if args is None: + args = () + if kwargs is None: + kwargs = {} + args = (func,) + args + + def _callback(func, *a, **kw): + self.wait(states.STARTED) + func(*a, **kw) + t = threading.Thread(target=_callback, args=args, kwargs=kwargs) + t.setName('Bus Callback ' + t.getName()) + t.start() + + self.start() + + return t + + def log(self, msg='', level=20, traceback=False): + """Log the given message. Append the last traceback if requested.""" + if traceback: + msg += '\n' + ''.join(_traceback.format_exception(*sys.exc_info())) + self.publish('log', msg, level) + + +bus = Bus() diff --git a/resources/lib/cherrypy/scaffold/__init__.py b/resources/lib/cherrypy/scaffold/__init__.py new file mode 100644 index 0000000..bcddba2 --- /dev/null +++ b/resources/lib/cherrypy/scaffold/__init__.py @@ -0,0 +1,63 @@ +""", a CherryPy application. + +Use this as a base for creating new CherryPy applications. When you want +to make a new app, copy and paste this folder to some other location +(maybe site-packages) and rename it to the name of your project, +then tweak as desired. + +Even before any tweaking, this should serve a few demonstration pages. +Change to this directory and run: + + cherryd -c site.conf + +""" + +import cherrypy +from cherrypy import tools, url + +import os +local_dir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +@cherrypy.config(**{'tools.log_tracebacks.on': True}) +class Root: + """Declaration of the CherryPy app URI structure.""" + + @cherrypy.expose + def index(self): + """Render HTML-template at the root path of the web-app.""" + return """ +Try some other path, +or a default path.
+Or, just look at the pretty picture:
+ +""" % (url('other'), url('else'), + url('files/made_with_cherrypy_small.png')) + + @cherrypy.expose + def default(self, *args, **kwargs): + """Render catch-all args and kwargs.""" + return 'args: %s kwargs: %s' % (args, kwargs) + + @cherrypy.expose + def other(self, a=2, b='bananas', c=None): + """Render number of fruits based on third argument.""" + cherrypy.response.headers['Content-Type'] = 'text/plain' + if c is None: + return 'Have %d %s.' % (int(a), b) + else: + return 'Have %d %s, %s.' % (int(a), b, c) + + files = tools.staticdir.handler( + section='/files', + dir=os.path.join(local_dir, 'static'), + # Ignore .php files, etc. + match=r'\.(css|gif|html?|ico|jpe?g|js|png|swf|xml)$', + ) + + +root = Root() + +# Uncomment the following to use your own favicon instead of CP's default. +# favicon_path = os.path.join(local_dir, "favicon.ico") +# root.favicon_ico = tools.staticfile.handler(filename=favicon_path) diff --git a/resources/lib/cherrypy/scaffold/apache-fcgi.conf b/resources/lib/cherrypy/scaffold/apache-fcgi.conf new file mode 100644 index 0000000..6e4f144 --- /dev/null +++ b/resources/lib/cherrypy/scaffold/apache-fcgi.conf @@ -0,0 +1,22 @@ +# Apache2 server conf file for using CherryPy with mod_fcgid. + +# This doesn't have to be "C:/", but it has to be a directory somewhere, and +# MUST match the directory used in the FastCgiExternalServer directive, below. +DocumentRoot "C:/" + +ServerName 127.0.0.1 +Listen 80 +LoadModule fastcgi_module modules/mod_fastcgi.dll +LoadModule rewrite_module modules/mod_rewrite.so + +Options ExecCGI +SetHandler fastcgi-script +RewriteEngine On +# Send requests for any URI to our fastcgi handler. +RewriteRule ^(.*)$ /fastcgi.pyc [L] + +# The FastCgiExternalServer directive defines filename as an external FastCGI application. +# If filename does not begin with a slash (/) then it is assumed to be relative to the ServerRoot. +# The filename does not have to exist in the local filesystem. URIs that Apache resolves to this +# filename will be handled by this external FastCGI application. +FastCgiExternalServer "C:/fastcgi.pyc" -host 127.0.0.1:8088 diff --git a/resources/lib/cherrypy/scaffold/example.conf b/resources/lib/cherrypy/scaffold/example.conf new file mode 100644 index 0000000..63250fe --- /dev/null +++ b/resources/lib/cherrypy/scaffold/example.conf @@ -0,0 +1,3 @@ +[/] +log.error_file: "error.log" +log.access_file: "access.log" diff --git a/resources/lib/cherrypy/scaffold/site.conf b/resources/lib/cherrypy/scaffold/site.conf new file mode 100644 index 0000000..6ed3898 --- /dev/null +++ b/resources/lib/cherrypy/scaffold/site.conf @@ -0,0 +1,14 @@ +[global] +# Uncomment this when you're done developing +#environment: "production" + +server.socket_host: "0.0.0.0" +server.socket_port: 8088 + +# Uncomment the following lines to run on HTTPS at the same time +#server.2.socket_host: "0.0.0.0" +#server.2.socket_port: 8433 +#server.2.ssl_certificate: '../test/test.pem' +#server.2.ssl_private_key: '../test/test.pem' + +tree.myapp: cherrypy.Application(scaffold.root, "/", "example.conf") diff --git a/resources/lib/cherrypy/scaffold/static/made_with_cherrypy_small.png b/resources/lib/cherrypy/scaffold/static/made_with_cherrypy_small.png new file mode 100644 index 0000000000000000000000000000000000000000..724f9d72d9ca5aede0b788fc1216286aa967e212 GIT binary patch literal 6347 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 N002ovPDHLkV1nY?RmA`R literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/test/__init__.py b/resources/lib/cherrypy/test/__init__.py new file mode 100644 index 0000000..068382b --- /dev/null +++ b/resources/lib/cherrypy/test/__init__.py @@ -0,0 +1,24 @@ +""" +Regression test suite for CherryPy. +""" + +import os +import sys + + +def newexit(): + os._exit(1) + + +def setup(): + # We want to monkey patch sys.exit so that we can get some + # information about where exit is being called. + newexit._old = sys.exit + sys.exit = newexit + + +def teardown(): + try: + sys.exit = sys.exit._old + except AttributeError: + sys.exit = sys._exit diff --git a/resources/lib/cherrypy/test/_test_decorators.py b/resources/lib/cherrypy/test/_test_decorators.py new file mode 100644 index 0000000..74832e4 --- /dev/null +++ b/resources/lib/cherrypy/test/_test_decorators.py @@ -0,0 +1,39 @@ +"""Test module for the @-decorator syntax, which is version-specific""" + +import cherrypy +from cherrypy import expose, tools + + +class ExposeExamples(object): + + @expose + def no_call(self): + return 'Mr E. R. Bradshaw' + + @expose() + def call_empty(self): + return 'Mrs. B.J. Smegma' + + @expose('call_alias') + def nesbitt(self): + return 'Mr Nesbitt' + + @expose(['alias1', 'alias2']) + def andrews(self): + return 'Mr Ken Andrews' + + @expose(alias='alias3') + def watson(self): + return 'Mr. and Mrs. Watson' + + +class ToolExamples(object): + + @expose + # This is here to demonstrate that using the config decorator + # does not overwrite other config attributes added by the Tool + # decorator (in this case response_headers). + @cherrypy.config(**{'response.stream': True}) + @tools.response_headers(headers=[('Content-Type', 'application/data')]) + def blah(self): + yield b'blah' diff --git a/resources/lib/cherrypy/test/_test_states_demo.py b/resources/lib/cherrypy/test/_test_states_demo.py new file mode 100644 index 0000000..a49407b --- /dev/null +++ b/resources/lib/cherrypy/test/_test_states_demo.py @@ -0,0 +1,69 @@ +import os +import sys +import time + +import cherrypy + +starttime = time.time() + + +class Root: + + @cherrypy.expose + def index(self): + return 'Hello World' + + @cherrypy.expose + def mtimes(self): + return repr(cherrypy.engine.publish('Autoreloader', 'mtimes')) + + @cherrypy.expose + def pid(self): + return str(os.getpid()) + + @cherrypy.expose + def start(self): + return repr(starttime) + + @cherrypy.expose + def exit(self): + # This handler might be called before the engine is STARTED if an + # HTTP worker thread handles it before the HTTP server returns + # control to engine.start. We avoid that race condition here + # by waiting for the Bus to be STARTED. + cherrypy.engine.wait(state=cherrypy.engine.states.STARTED) + cherrypy.engine.exit() + + +@cherrypy.engine.subscribe('start', priority=100) +def unsub_sig(): + cherrypy.log('unsubsig: %s' % cherrypy.config.get('unsubsig', False)) + if cherrypy.config.get('unsubsig', False): + cherrypy.log('Unsubscribing the default cherrypy signal handler') + cherrypy.engine.signal_handler.unsubscribe() + try: + from signal import signal, SIGTERM + except ImportError: + pass + else: + def old_term_handler(signum=None, frame=None): + cherrypy.log('I am an old SIGTERM handler.') + sys.exit(0) + cherrypy.log('Subscribing the new one.') + signal(SIGTERM, old_term_handler) + + +@cherrypy.engine.subscribe('start', priority=6) +def starterror(): + if cherrypy.config.get('starterror', False): + 1 / 0 + + +@cherrypy.engine.subscribe('start', priority=6) +def log_test_case_name(): + if cherrypy.config.get('test_case_name', False): + cherrypy.log('STARTED FROM: %s' % + cherrypy.config.get('test_case_name')) + + +cherrypy.tree.mount(Root(), '/', {'/': {}}) diff --git a/resources/lib/cherrypy/test/benchmark.py b/resources/lib/cherrypy/test/benchmark.py new file mode 100644 index 0000000..44dfeff --- /dev/null +++ b/resources/lib/cherrypy/test/benchmark.py @@ -0,0 +1,425 @@ +"""CherryPy Benchmark Tool + + Usage: + benchmark.py [options] + + --null: use a null Request object (to bench the HTTP server only) + --notests: start the server but do not run the tests; this allows + you to check the tested pages with a browser + --help: show this help message + --cpmodpy: run tests via apache on 54583 (with the builtin _cpmodpy) + --modpython: run tests via apache on 54583 (with modpython_gateway) + --ab=path: Use the ab script/executable at 'path' (see below) + --apache=path: Use the apache script/exe at 'path' (see below) + + To run the benchmarks, the Apache Benchmark tool "ab" must either be on + your system path, or specified via the --ab=path option. + + To run the modpython tests, the "apache" executable or script must be + on your system path, or provided via the --apache=path option. On some + platforms, "apache" may be called "apachectl" or "apache2ctl"--create + a symlink to them if needed. +""" + +import getopt +import os +import re +import sys +import time + +import cherrypy +from cherrypy import _cperror, _cpmodpy +from cherrypy.lib import httputil + + +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + +AB_PATH = '' +APACHE_PATH = 'apache' +SCRIPT_NAME = '/cpbench/users/rdelon/apps/blog' + +__all__ = ['ABSession', 'Root', 'print_report', + 'run_standard_benchmarks', 'safe_threads', + 'size_report', 'thread_report', + ] + +size_cache = {} + + +class Root: + + @cherrypy.expose + def index(self): + return """ + + CherryPy Benchmark + + +

+ +""" + + @cherrypy.expose + def hello(self): + return 'Hello, world\r\n' + + @cherrypy.expose + def sizer(self, size): + resp = size_cache.get(size, None) + if resp is None: + size_cache[size] = resp = 'X' * int(size) + return resp + + +def init(): + + cherrypy.config.update({ + 'log.error.file': '', + 'environment': 'production', + 'server.socket_host': '127.0.0.1', + 'server.socket_port': 54583, + 'server.max_request_header_size': 0, + 'server.max_request_body_size': 0, + }) + + # Cheat mode on ;) + del cherrypy.config['tools.log_tracebacks.on'] + del cherrypy.config['tools.log_headers.on'] + del cherrypy.config['tools.trailing_slash.on'] + + appconf = { + '/static': { + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.root': curdir, + }, + } + globals().update( + app=cherrypy.tree.mount(Root(), SCRIPT_NAME, appconf), + ) + + +class NullRequest: + + """A null HTTP request class, returning 200 and an empty body.""" + + def __init__(self, local, remote, scheme='http'): + pass + + def close(self): + pass + + def run(self, method, path, query_string, protocol, headers, rfile): + cherrypy.response.status = '200 OK' + cherrypy.response.header_list = [('Content-Type', 'text/html'), + ('Server', 'Null CherryPy'), + ('Date', httputil.HTTPDate()), + ('Content-Length', '0'), + ] + cherrypy.response.body = [''] + return cherrypy.response + + +class NullResponse: + pass + + +class ABSession: + + """A session of 'ab', the Apache HTTP server benchmarking tool. + +Example output from ab: + +This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.1 $> apache-2.0 +Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/ +Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/ + +Benchmarking 127.0.0.1 (be patient) +Completed 100 requests +Completed 200 requests +Completed 300 requests +Completed 400 requests +Completed 500 requests +Completed 600 requests +Completed 700 requests +Completed 800 requests +Completed 900 requests + + +Server Software: CherryPy/3.1beta +Server Hostname: 127.0.0.1 +Server Port: 54583 + +Document Path: /static/index.html +Document Length: 14 bytes + +Concurrency Level: 10 +Time taken for tests: 9.643867 seconds +Complete requests: 1000 +Failed requests: 0 +Write errors: 0 +Total transferred: 189000 bytes +HTML transferred: 14000 bytes +Requests per second: 103.69 [#/sec] (mean) +Time per request: 96.439 [ms] (mean) +Time per request: 9.644 [ms] (mean, across all concurrent requests) +Transfer rate: 19.08 [Kbytes/sec] received + +Connection Times (ms) + min mean[+/-sd] median max +Connect: 0 0 2.9 0 10 +Processing: 20 94 7.3 90 130 +Waiting: 0 43 28.1 40 100 +Total: 20 95 7.3 100 130 + +Percentage of the requests served within a certain time (ms) + 50% 100 + 66% 100 + 75% 100 + 80% 100 + 90% 100 + 95% 100 + 98% 100 + 99% 110 + 100% 130 (longest request) +Finished 1000 requests +""" + + parse_patterns = [ + ('complete_requests', 'Completed', + br'^Complete requests:\s*(\d+)'), + ('failed_requests', 'Failed', + br'^Failed requests:\s*(\d+)'), + ('requests_per_second', 'req/sec', + br'^Requests per second:\s*([0-9.]+)'), + ('time_per_request_concurrent', 'msec/req', + br'^Time per request:\s*([0-9.]+).*concurrent requests\)$'), + ('transfer_rate', 'KB/sec', + br'^Transfer rate:\s*([0-9.]+)') + ] + + def __init__(self, path=SCRIPT_NAME + '/hello', requests=1000, + concurrency=10): + self.path = path + self.requests = requests + self.concurrency = concurrency + + def args(self): + port = cherrypy.server.socket_port + assert self.concurrency > 0 + assert self.requests > 0 + # Don't use "localhost". + # Cf + # http://mail.python.org/pipermail/python-win32/2008-March/007050.html + return ('-k -n %s -c %s http://127.0.0.1:%s%s' % + (self.requests, self.concurrency, port, self.path)) + + def run(self): + # Parse output of ab, setting attributes on self + try: + self.output = _cpmodpy.read_process(AB_PATH or 'ab', self.args()) + except Exception: + print(_cperror.format_exc()) + raise + + for attr, name, pattern in self.parse_patterns: + val = re.search(pattern, self.output, re.MULTILINE) + if val: + val = val.group(1) + setattr(self, attr, val) + else: + setattr(self, attr, None) + + +safe_threads = (25, 50, 100, 200, 400) +if sys.platform in ('win32',): + # For some reason, ab crashes with > 50 threads on my Win2k laptop. + safe_threads = (10, 20, 30, 40, 50) + + +def thread_report(path=SCRIPT_NAME + '/hello', concurrency=safe_threads): + sess = ABSession(path) + attrs, names, patterns = list(zip(*sess.parse_patterns)) + avg = dict.fromkeys(attrs, 0.0) + + yield ('threads',) + names + for c in concurrency: + sess.concurrency = c + sess.run() + row = [c] + for attr in attrs: + val = getattr(sess, attr) + if val is None: + print(sess.output) + row = None + break + val = float(val) + avg[attr] += float(val) + row.append(val) + if row: + yield row + + # Add a row of averages. + yield ['Average'] + [str(avg[attr] / len(concurrency)) for attr in attrs] + + +def size_report(sizes=(10, 100, 1000, 10000, 100000, 100000000), + concurrency=50): + sess = ABSession(concurrency=concurrency) + attrs, names, patterns = list(zip(*sess.parse_patterns)) + yield ('bytes',) + names + for sz in sizes: + sess.path = '%s/sizer?size=%s' % (SCRIPT_NAME, sz) + sess.run() + yield [sz] + [getattr(sess, attr) for attr in attrs] + + +def print_report(rows): + for row in rows: + print('') + for val in row: + sys.stdout.write(str(val).rjust(10) + ' | ') + print('') + + +def run_standard_benchmarks(): + print('') + print('Client Thread Report (1000 requests, 14 byte response body, ' + '%s server threads):' % cherrypy.server.thread_pool) + print_report(thread_report()) + + print('') + print('Client Thread Report (1000 requests, 14 bytes via staticdir, ' + '%s server threads):' % cherrypy.server.thread_pool) + print_report(thread_report('%s/static/index.html' % SCRIPT_NAME)) + + print('') + print('Size Report (1000 requests, 50 client threads, ' + '%s server threads):' % cherrypy.server.thread_pool) + print_report(size_report()) + + +# modpython and other WSGI # + +def startup_modpython(req=None): + """Start the CherryPy app server in 'serverless' mode (for modpython/WSGI). + """ + if cherrypy.engine.state == cherrypy._cpengine.STOPPED: + if req: + if 'nullreq' in req.get_options(): + cherrypy.engine.request_class = NullRequest + cherrypy.engine.response_class = NullResponse + ab_opt = req.get_options().get('ab', '') + if ab_opt: + global AB_PATH + AB_PATH = ab_opt + cherrypy.engine.start() + if cherrypy.engine.state == cherrypy._cpengine.STARTING: + cherrypy.engine.wait() + return 0 # apache.OK + + +def run_modpython(use_wsgi=False): + print('Starting mod_python...') + pyopts = [] + + # Pass the null and ab=path options through Apache + if '--null' in opts: + pyopts.append(('nullreq', '')) + + if '--ab' in opts: + pyopts.append(('ab', opts['--ab'])) + + s = _cpmodpy.ModPythonServer + if use_wsgi: + pyopts.append(('wsgi.application', 'cherrypy::tree')) + pyopts.append( + ('wsgi.startup', 'cherrypy.test.benchmark::startup_modpython')) + handler = 'modpython_gateway::handler' + s = s(port=54583, opts=pyopts, + apache_path=APACHE_PATH, handler=handler) + else: + pyopts.append( + ('cherrypy.setup', 'cherrypy.test.benchmark::startup_modpython')) + s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH) + + try: + s.start() + run() + finally: + s.stop() + + +if __name__ == '__main__': + init() + + longopts = ['cpmodpy', 'modpython', 'null', 'notests', + 'help', 'ab=', 'apache='] + try: + switches, args = getopt.getopt(sys.argv[1:], '', longopts) + opts = dict(switches) + except getopt.GetoptError: + print(__doc__) + sys.exit(2) + + if '--help' in opts: + print(__doc__) + sys.exit(0) + + if '--ab' in opts: + AB_PATH = opts['--ab'] + + if '--notests' in opts: + # Return without stopping the server, so that the pages + # can be tested from a standard web browser. + def run(): + port = cherrypy.server.socket_port + print('You may now open http://127.0.0.1:%s%s/' % + (port, SCRIPT_NAME)) + + if '--null' in opts: + print('Using null Request object') + else: + def run(): + end = time.time() - start + print('Started in %s seconds' % end) + if '--null' in opts: + print('\nUsing null Request object') + try: + try: + run_standard_benchmarks() + except Exception: + print(_cperror.format_exc()) + raise + finally: + cherrypy.engine.exit() + + print('Starting CherryPy app server...') + + class NullWriter(object): + + """Suppresses the printing of socket errors.""" + + def write(self, data): + pass + sys.stderr = NullWriter() + + start = time.time() + + if '--cpmodpy' in opts: + run_modpython() + elif '--modpython' in opts: + run_modpython(use_wsgi=True) + else: + if '--null' in opts: + cherrypy.server.request_class = NullRequest + cherrypy.server.response_class = NullResponse + + cherrypy.engine.start_with_callback(run) + cherrypy.engine.block() diff --git a/resources/lib/cherrypy/test/checkerdemo.py b/resources/lib/cherrypy/test/checkerdemo.py new file mode 100644 index 0000000..3438bd0 --- /dev/null +++ b/resources/lib/cherrypy/test/checkerdemo.py @@ -0,0 +1,49 @@ +"""Demonstration app for cherrypy.checker. + +This application is intentionally broken and badly designed. +To demonstrate the output of the CherryPy Checker, simply execute +this module. +""" + +import os +import cherrypy +thisdir = os.path.dirname(os.path.abspath(__file__)) + + +class Root: + pass + + +if __name__ == '__main__': + conf = {'/base': {'tools.staticdir.root': thisdir, + # Obsolete key. + 'throw_errors': True, + }, + # This entry should be OK. + '/base/static': {'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static'}, + # Warn on missing folder. + '/base/js': {'tools.staticdir.on': True, + 'tools.staticdir.dir': 'js'}, + # Warn on dir with an abs path even though we provide root. + '/base/static2': {'tools.staticdir.on': True, + 'tools.staticdir.dir': '/static'}, + # Warn on dir with a relative path with no root. + '/static3': {'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static'}, + # Warn on unknown namespace + '/unknown': {'toobles.gzip.on': True}, + # Warn special on cherrypy..* + '/cpknown': {'cherrypy.tools.encode.on': True}, + # Warn on mismatched types + '/conftype': {'request.show_tracebacks': 14}, + # Warn on unknown tool. + '/web': {'tools.unknown.on': True}, + # Warn on server.* in app config. + '/app1': {'server.socket_host': '0.0.0.0'}, + # Warn on 'localhost' + 'global': {'server.socket_host': 'localhost'}, + # Warn on '[name]' + '[/extra_brackets]': {}, + } + cherrypy.quickstart(Root(), config=conf) diff --git a/resources/lib/cherrypy/test/fastcgi.conf b/resources/lib/cherrypy/test/fastcgi.conf new file mode 100644 index 0000000..e5c5163 --- /dev/null +++ b/resources/lib/cherrypy/test/fastcgi.conf @@ -0,0 +1,18 @@ + +# Apache2 server conf file for testing CherryPy with mod_fastcgi. +# fumanchu: I had to hard-code paths due to crazy Debian layouts :( +ServerRoot /usr/lib/apache2 +User #1000 +ErrorLog /usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/mod_fastcgi.error.log + +DocumentRoot "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test" +ServerName 127.0.0.1 +Listen 8080 +LoadModule fastcgi_module modules/mod_fastcgi.so +LoadModule rewrite_module modules/mod_rewrite.so + +Options +ExecCGI +SetHandler fastcgi-script +RewriteEngine On +RewriteRule ^(.*)$ /fastcgi.pyc [L] +FastCgiExternalServer "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000 diff --git a/resources/lib/cherrypy/test/fcgi.conf b/resources/lib/cherrypy/test/fcgi.conf new file mode 100644 index 0000000..3062eb3 --- /dev/null +++ b/resources/lib/cherrypy/test/fcgi.conf @@ -0,0 +1,14 @@ + +# Apache2 server conf file for testing CherryPy with mod_fcgid. + +DocumentRoot "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test" +ServerName 127.0.0.1 +Listen 8080 +LoadModule fastcgi_module modules/mod_fastcgi.dll +LoadModule rewrite_module modules/mod_rewrite.so + +Options ExecCGI +SetHandler fastcgi-script +RewriteEngine On +RewriteRule ^(.*)$ /fastcgi.pyc [L] +FastCgiExternalServer "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000 diff --git a/resources/lib/cherrypy/test/helper.py b/resources/lib/cherrypy/test/helper.py new file mode 100644 index 0000000..01c5a0c --- /dev/null +++ b/resources/lib/cherrypy/test/helper.py @@ -0,0 +1,542 @@ +"""A library of helper functions for the CherryPy test suite.""" + +import datetime +import io +import logging +import os +import re +import subprocess +import sys +import time +import unittest +import warnings + +import portend +import pytest +import six + +from cheroot.test import webtest + +import cherrypy +from cherrypy._cpcompat import text_or_bytes, HTTPSConnection, ntob +from cherrypy.lib import httputil +from cherrypy.lib import gctools + +log = logging.getLogger(__name__) +thisdir = os.path.abspath(os.path.dirname(__file__)) +serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem') + + +class Supervisor(object): + + """Base class for modeling and controlling servers during testing.""" + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + if k == 'port': + setattr(self, k, int(v)) + setattr(self, k, v) + + +def log_to_stderr(msg, level): + return sys.stderr.write(msg + os.linesep) + + +class LocalSupervisor(Supervisor): + + """Base class for modeling/controlling servers which run in the same + process. + + When the server side runs in a different process, start/stop can dump all + state between each test module easily. When the server side runs in the + same process as the client, however, we have to do a bit more work to + ensure config and mounted apps are reset between tests. + """ + + using_apache = False + using_wsgi = False + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + cherrypy.server.httpserver = self.httpserver_class + + # This is perhaps the wrong place for this call but this is the only + # place that i've found so far that I KNOW is early enough to set this. + cherrypy.config.update({'log.screen': False}) + engine = cherrypy.engine + if hasattr(engine, 'signal_handler'): + engine.signal_handler.subscribe() + if hasattr(engine, 'console_control_handler'): + engine.console_control_handler.subscribe() + + def start(self, modulename=None): + """Load and start the HTTP server.""" + if modulename: + # Unhook httpserver so cherrypy.server.start() creates a new + # one (with config from setup_server, if declared). + cherrypy.server.httpserver = None + + cherrypy.engine.start() + + self.sync_apps() + + def sync_apps(self): + """Tell the server about any apps which the setup functions mounted.""" + pass + + def stop(self): + td = getattr(self, 'teardown', None) + if td: + td() + + cherrypy.engine.exit() + + servers_copy = list(six.iteritems(getattr(cherrypy, 'servers', {}))) + for name, server in servers_copy: + server.unsubscribe() + del cherrypy.servers[name] + + +class NativeServerSupervisor(LocalSupervisor): + + """Server supervisor for the builtin HTTP server.""" + + httpserver_class = 'cherrypy._cpnative_server.CPHTTPServer' + using_apache = False + using_wsgi = False + + def __str__(self): + return 'Builtin HTTP Server on %s:%s' % (self.host, self.port) + + +class LocalWSGISupervisor(LocalSupervisor): + + """Server supervisor for the builtin WSGI server.""" + + httpserver_class = 'cherrypy._cpwsgi_server.CPWSGIServer' + using_apache = False + using_wsgi = True + + def __str__(self): + return 'Builtin WSGI Server on %s:%s' % (self.host, self.port) + + def sync_apps(self): + """Hook a new WSGI app into the origin server.""" + cherrypy.server.httpserver.wsgi_app = self.get_app() + + def get_app(self, app=None): + """Obtain a new (decorated) WSGI app to hook into the origin server.""" + if app is None: + app = cherrypy.tree + + if self.validate: + try: + from wsgiref import validate + except ImportError: + warnings.warn( + 'Error importing wsgiref. The validator will not run.') + else: + # wraps the app in the validator + app = validate.validator(app) + + return app + + +def get_cpmodpy_supervisor(**options): + from cherrypy.test import modpy + sup = modpy.ModPythonSupervisor(**options) + sup.template = modpy.conf_cpmodpy + return sup + + +def get_modpygw_supervisor(**options): + from cherrypy.test import modpy + sup = modpy.ModPythonSupervisor(**options) + sup.template = modpy.conf_modpython_gateway + sup.using_wsgi = True + return sup + + +def get_modwsgi_supervisor(**options): + from cherrypy.test import modwsgi + return modwsgi.ModWSGISupervisor(**options) + + +def get_modfcgid_supervisor(**options): + from cherrypy.test import modfcgid + return modfcgid.ModFCGISupervisor(**options) + + +def get_modfastcgi_supervisor(**options): + from cherrypy.test import modfastcgi + return modfastcgi.ModFCGISupervisor(**options) + + +def get_wsgi_u_supervisor(**options): + cherrypy.server.wsgi_version = ('u', 0) + return LocalWSGISupervisor(**options) + + +class CPWebCase(webtest.WebCase): + + script_name = '' + scheme = 'http' + + available_servers = {'wsgi': LocalWSGISupervisor, + 'wsgi_u': get_wsgi_u_supervisor, + 'native': NativeServerSupervisor, + 'cpmodpy': get_cpmodpy_supervisor, + 'modpygw': get_modpygw_supervisor, + 'modwsgi': get_modwsgi_supervisor, + 'modfcgid': get_modfcgid_supervisor, + 'modfastcgi': get_modfastcgi_supervisor, + } + default_server = 'wsgi' + + @classmethod + def _setup_server(cls, supervisor, conf): + v = sys.version.split()[0] + log.info('Python version used to run this test script: %s' % v) + log.info('CherryPy version: %s' % cherrypy.__version__) + if supervisor.scheme == 'https': + ssl = ' (ssl)' + else: + ssl = '' + log.info('HTTP server version: %s%s' % (supervisor.protocol, ssl)) + log.info('PID: %s' % os.getpid()) + + cherrypy.server.using_apache = supervisor.using_apache + cherrypy.server.using_wsgi = supervisor.using_wsgi + + if sys.platform[:4] == 'java': + cherrypy.config.update({'server.nodelay': False}) + + if isinstance(conf, text_or_bytes): + parser = cherrypy.lib.reprconf.Parser() + conf = parser.dict_from_file(conf).get('global', {}) + else: + conf = conf or {} + baseconf = conf.copy() + baseconf.update({'server.socket_host': supervisor.host, + 'server.socket_port': supervisor.port, + 'server.protocol_version': supervisor.protocol, + 'environment': 'test_suite', + }) + if supervisor.scheme == 'https': + # baseconf['server.ssl_module'] = 'builtin' + baseconf['server.ssl_certificate'] = serverpem + baseconf['server.ssl_private_key'] = serverpem + + # helper must be imported lazily so the coverage tool + # can run against module-level statements within cherrypy. + # Also, we have to do "from cherrypy.test import helper", + # exactly like each test module does, because a relative import + # would stick a second instance of webtest in sys.modules, + # and we wouldn't be able to globally override the port anymore. + if supervisor.scheme == 'https': + webtest.WebCase.HTTP_CONN = HTTPSConnection + return baseconf + + @classmethod + def setup_class(cls): + '' + # Creates a server + conf = { + 'scheme': 'http', + 'protocol': 'HTTP/1.1', + 'port': 54583, + 'host': '127.0.0.1', + 'validate': False, + 'server': 'wsgi', + } + supervisor_factory = cls.available_servers.get( + conf.get('server', 'wsgi')) + if supervisor_factory is None: + raise RuntimeError('Unknown server in config: %s' % conf['server']) + supervisor = supervisor_factory(**conf) + + # Copied from "run_test_suite" + cherrypy.config.reset() + baseconf = cls._setup_server(supervisor, conf) + cherrypy.config.update(baseconf) + setup_client() + + if hasattr(cls, 'setup_server'): + # Clear the cherrypy tree and clear the wsgi server so that + # it can be updated with the new root + cherrypy.tree = cherrypy._cptree.Tree() + cherrypy.server.httpserver = None + cls.setup_server() + # Add a resource for verifying there are no refleaks + # to *every* test class. + cherrypy.tree.mount(gctools.GCRoot(), '/gc') + cls.do_gc_test = True + supervisor.start(cls.__module__) + + cls.supervisor = supervisor + + @classmethod + def teardown_class(cls): + '' + if hasattr(cls, 'setup_server'): + cls.supervisor.stop() + + do_gc_test = False + + def test_gc(self): + if not self.do_gc_test: + return + + self.getPage('/gc/stats') + try: + self.assertBody('Statistics:') + except Exception: + 'Failures occur intermittently. See #1420' + + def prefix(self): + return self.script_name.rstrip('/') + + def base(self): + if ((self.scheme == 'http' and self.PORT == 80) or + (self.scheme == 'https' and self.PORT == 443)): + port = '' + else: + port = ':%s' % self.PORT + + return '%s://%s%s%s' % (self.scheme, self.HOST, port, + self.script_name.rstrip('/')) + + def exit(self): + sys.exit() + + def getPage(self, url, headers=None, method='GET', body=None, + protocol=None, raise_subcls=None): + """Open the url. Return status, headers, body. + + `raise_subcls` must be a tuple with the exceptions classes + or a single exception class that are not going to be considered + a socket.error regardless that they were are subclass of a + socket.error and therefore not considered for a connection retry. + """ + if self.script_name: + url = httputil.urljoin(self.script_name, url) + return webtest.WebCase.getPage(self, url, headers, method, body, + protocol, raise_subcls) + + def skip(self, msg='skipped '): + pytest.skip(msg) + + def assertErrorPage(self, status, message=None, pattern=''): + """Compare the response body with a built in error page. + + The function will optionally look for the regexp pattern, + within the exception embedded in the error page.""" + + # This will never contain a traceback + page = cherrypy._cperror.get_error_page(status, message=message) + + # First, test the response body without checking the traceback. + # Stick a match-all group (.*) in to grab the traceback. + def esc(text): + return re.escape(ntob(text)) + epage = re.escape(page) + epage = epage.replace( + esc('
'),
+            esc('
') + b'(.*)' + esc('
')) + m = re.match(epage, self.body, re.DOTALL) + if not m: + self._handlewebError( + 'Error page does not match; expected:\n' + page) + return + + # Now test the pattern against the traceback + if pattern is None: + # Special-case None to mean that there should be *no* traceback. + if m and m.group(1): + self._handlewebError('Error page contains traceback') + else: + if (m is None) or ( + not re.search(ntob(re.escape(pattern), self.encoding), + m.group(1))): + msg = 'Error page does not contain %s in traceback' + self._handlewebError(msg % repr(pattern)) + + date_tolerance = 2 + + def assertEqualDates(self, dt1, dt2, seconds=None): + """Assert abs(dt1 - dt2) is within Y seconds.""" + if seconds is None: + seconds = self.date_tolerance + + if dt1 > dt2: + diff = dt1 - dt2 + else: + diff = dt2 - dt1 + if not diff < datetime.timedelta(seconds=seconds): + raise AssertionError('%r and %r are not within %r seconds.' % + (dt1, dt2, seconds)) + + +def _test_method_sorter(_, x, y): + """Monkeypatch the test sorter to always run test_gc last in each suite.""" + if x == 'test_gc': + return 1 + if y == 'test_gc': + return -1 + if x > y: + return 1 + if x < y: + return -1 + return 0 + + +unittest.TestLoader.sortTestMethodsUsing = _test_method_sorter + + +def setup_client(): + """Set up the WebCase classes to match the server's socket settings.""" + webtest.WebCase.PORT = cherrypy.server.socket_port + webtest.WebCase.HOST = cherrypy.server.socket_host + if cherrypy.server.ssl_certificate: + CPWebCase.scheme = 'https' + +# --------------------------- Spawning helpers --------------------------- # + + +class CPProcess(object): + + pid_file = os.path.join(thisdir, 'test.pid') + config_file = os.path.join(thisdir, 'test.conf') + config_template = """[global] +server.socket_host: '%(host)s' +server.socket_port: %(port)s +checker.on: False +log.screen: False +log.error_file: r'%(error_log)s' +log.access_file: r'%(access_log)s' +%(ssl)s +%(extra)s +""" + error_log = os.path.join(thisdir, 'test.error.log') + access_log = os.path.join(thisdir, 'test.access.log') + + def __init__(self, wait=False, daemonize=False, ssl=False, + socket_host=None, socket_port=None): + self.wait = wait + self.daemonize = daemonize + self.ssl = ssl + self.host = socket_host or cherrypy.server.socket_host + self.port = socket_port or cherrypy.server.socket_port + + def write_conf(self, extra=''): + if self.ssl: + serverpem = os.path.join(thisdir, 'test.pem') + ssl = """ +server.ssl_certificate: r'%s' +server.ssl_private_key: r'%s' +""" % (serverpem, serverpem) + else: + ssl = '' + + conf = self.config_template % { + 'host': self.host, + 'port': self.port, + 'error_log': self.error_log, + 'access_log': self.access_log, + 'ssl': ssl, + 'extra': extra, + } + with io.open(self.config_file, 'w', encoding='utf-8') as f: + f.write(six.text_type(conf)) + + def start(self, imports=None): + """Start cherryd in a subprocess.""" + portend.free(self.host, self.port, timeout=1) + + args = [ + '-m', + 'cherrypy', + '-c', self.config_file, + '-p', self.pid_file, + ] + r""" + Command for running cherryd server with autoreload enabled + + Using + + ``` + ['-c', + "__requires__ = 'CherryPy'; \ + import pkg_resources, re, sys; \ + sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]); \ + sys.exit(\ + pkg_resources.load_entry_point(\ + 'CherryPy', 'console_scripts', 'cherryd')())"] + ``` + + doesn't work as it's impossible to reconstruct the `-c`'s contents. + Ref: https://github.com/cherrypy/cherrypy/issues/1545 + """ + + if not isinstance(imports, (list, tuple)): + imports = [imports] + for i in imports: + if i: + args.append('-i') + args.append(i) + + if self.daemonize: + args.append('-d') + + env = os.environ.copy() + # Make sure we import the cherrypy package in which this module is + # defined. + grandparentdir = os.path.abspath(os.path.join(thisdir, '..', '..')) + if env.get('PYTHONPATH', ''): + env['PYTHONPATH'] = os.pathsep.join( + (grandparentdir, env['PYTHONPATH'])) + else: + env['PYTHONPATH'] = grandparentdir + self._proc = subprocess.Popen([sys.executable] + args, env=env) + if self.wait: + self.exit_code = self._proc.wait() + else: + portend.occupied(self.host, self.port, timeout=5) + + # Give the engine a wee bit more time to finish STARTING + if self.daemonize: + time.sleep(2) + else: + time.sleep(1) + + def get_pid(self): + if self.daemonize: + return int(open(self.pid_file, 'rb').read()) + return self._proc.pid + + def join(self): + """Wait for the process to exit.""" + if self.daemonize: + return self._join_daemon() + self._proc.wait() + + def _join_daemon(self): + try: + try: + # Mac, UNIX + os.wait() + except AttributeError: + # Windows + try: + pid = self.get_pid() + except IOError: + # Assume the subprocess deleted the pidfile on shutdown. + pass + else: + os.waitpid(pid, 0) + except OSError: + x = sys.exc_info()[1] + if x.args != (10, 'No child processes'): + raise diff --git a/resources/lib/cherrypy/test/logtest.py b/resources/lib/cherrypy/test/logtest.py new file mode 100644 index 0000000..ed8f154 --- /dev/null +++ b/resources/lib/cherrypy/test/logtest.py @@ -0,0 +1,228 @@ +"""logtest, a unittest.TestCase helper for testing log output.""" + +import sys +import time +from uuid import UUID + +import six + +from cherrypy._cpcompat import text_or_bytes, ntob + + +try: + # On Windows, msvcrt.getch reads a single char without output. + import msvcrt + + def getchar(): + return msvcrt.getch() +except ImportError: + # Unix getchr + import tty + import termios + + def getchar(): + fd = sys.stdin.fileno() + old_settings = termios.tcgetattr(fd) + try: + tty.setraw(sys.stdin.fileno()) + ch = sys.stdin.read(1) + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + return ch + + +class LogCase(object): + + """unittest.TestCase mixin for testing log messages. + + logfile: a filename for the desired log. Yes, I know modes are evil, + but it makes the test functions so much cleaner to set this once. + + lastmarker: the last marker in the log. This can be used to search for + messages since the last marker. + + markerPrefix: a string with which to prefix log markers. This should be + unique enough from normal log output to use for marker identification. + """ + + logfile = None + lastmarker = None + markerPrefix = b'test suite marker: ' + + def _handleLogError(self, msg, data, marker, pattern): + print('') + print(' ERROR: %s' % msg) + + if not self.interactive: + raise self.failureException(msg) + + p = (' Show: ' + '[L]og [M]arker [P]attern; ' + '[I]gnore, [R]aise, or sys.e[X]it >> ') + sys.stdout.write(p + ' ') + # ARGH + sys.stdout.flush() + while True: + i = getchar().upper() + if i not in 'MPLIRX': + continue + print(i.upper()) # Also prints new line + if i == 'L': + for x, line in enumerate(data): + if (x + 1) % self.console_height == 0: + # The \r and comma should make the next line overwrite + sys.stdout.write('<-- More -->\r ') + m = getchar().lower() + # Erase our "More" prompt + sys.stdout.write(' \r ') + if m == 'q': + break + print(line.rstrip()) + elif i == 'M': + print(repr(marker or self.lastmarker)) + elif i == 'P': + print(repr(pattern)) + elif i == 'I': + # return without raising the normal exception + return + elif i == 'R': + raise self.failureException(msg) + elif i == 'X': + self.exit() + sys.stdout.write(p + ' ') + + def exit(self): + sys.exit() + + def emptyLog(self): + """Overwrite self.logfile with 0 bytes.""" + open(self.logfile, 'wb').write('') + + def markLog(self, key=None): + """Insert a marker line into the log and set self.lastmarker.""" + if key is None: + key = str(time.time()) + self.lastmarker = key + + open(self.logfile, 'ab+').write( + ntob('%s%s\n' % (self.markerPrefix, key), 'utf-8')) + + def _read_marked_region(self, marker=None): + """Return lines from self.logfile in the marked region. + + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be returned. + """ +# Give the logger time to finish writing? +# time.sleep(0.5) + + logfile = self.logfile + marker = marker or self.lastmarker + if marker is None: + return open(logfile, 'rb').readlines() + + if isinstance(marker, six.text_type): + marker = marker.encode('utf-8') + data = [] + in_region = False + for line in open(logfile, 'rb'): + if in_region: + if line.startswith(self.markerPrefix) and marker not in line: + break + else: + data.append(line) + elif marker in line: + in_region = True + return data + + def assertInLog(self, line, marker=None): + """Fail if the given (partial) line is not in the log. + + The log will be searched from the given marker to the next marker. + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be searched. + """ + data = self._read_marked_region(marker) + for logline in data: + if line in logline: + return + msg = '%r not found in log' % line + self._handleLogError(msg, data, marker, line) + + def assertNotInLog(self, line, marker=None): + """Fail if the given (partial) line is in the log. + + The log will be searched from the given marker to the next marker. + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be searched. + """ + data = self._read_marked_region(marker) + for logline in data: + if line in logline: + msg = '%r found in log' % line + self._handleLogError(msg, data, marker, line) + + def assertValidUUIDv4(self, marker=None): + """Fail if the given UUIDv4 is not valid. + + The log will be searched from the given marker to the next marker. + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be searched. + """ + data = self._read_marked_region(marker) + data = [ + chunk.decode('utf-8').rstrip('\n').rstrip('\r') + for chunk in data + ] + for log_chunk in data: + try: + uuid_log = data[-1] + uuid_obj = UUID(uuid_log, version=4) + except (TypeError, ValueError): + pass # it might be in other chunk + else: + if str(uuid_obj) == uuid_log: + return + msg = '%r is not a valid UUIDv4' % uuid_log + self._handleLogError(msg, data, marker, log_chunk) + + msg = 'UUIDv4 not found in log' + self._handleLogError(msg, data, marker, log_chunk) + + def assertLog(self, sliceargs, lines, marker=None): + """Fail if log.readlines()[sliceargs] is not contained in 'lines'. + + The log will be searched from the given marker to the next marker. + If marker is None, self.lastmarker is used. If the log hasn't + been marked (using self.markLog), the entire log will be searched. + """ + data = self._read_marked_region(marker) + if isinstance(sliceargs, int): + # Single arg. Use __getitem__ and allow lines to be str or list. + if isinstance(lines, (tuple, list)): + lines = lines[0] + if isinstance(lines, six.text_type): + lines = lines.encode('utf-8') + if lines not in data[sliceargs]: + msg = '%r not found on log line %r' % (lines, sliceargs) + self._handleLogError( + msg, + [data[sliceargs], '--EXTRA CONTEXT--'] + data[ + sliceargs + 1:sliceargs + 6], + marker, + lines) + else: + # Multiple args. Use __getslice__ and require lines to be list. + if isinstance(lines, tuple): + lines = list(lines) + elif isinstance(lines, text_or_bytes): + raise TypeError("The 'lines' arg must be a list when " + "'sliceargs' is a tuple.") + + start, stop = sliceargs + for line, logline in zip(lines, data[start:stop]): + if isinstance(line, six.text_type): + line = line.encode('utf-8') + if line not in logline: + msg = '%r not found in log' % line + self._handleLogError(msg, data[start:stop], marker, line) diff --git a/resources/lib/cherrypy/test/modfastcgi.py b/resources/lib/cherrypy/test/modfastcgi.py new file mode 100644 index 0000000..79ec3d1 --- /dev/null +++ b/resources/lib/cherrypy/test/modfastcgi.py @@ -0,0 +1,136 @@ +"""Wrapper for mod_fastcgi, for use as a CherryPy HTTP server when testing. + +To autostart fastcgi, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl", "apache2ctl", +or "httpd"--create a symlink to them if needed. + +You'll also need the WSGIServer from flup.servers. +See http://projects.amor.org/misc/wiki/ModPythonGateway + + +KNOWN BUGS +========== + +1. Apache processes Range headers automatically; CherryPy's truncated + output is then truncated again by Apache. See test_core.testRanges. + This was worked around in http://www.cherrypy.org/changeset/1319. +2. Apache does not allow custom HTTP methods like CONNECT as per the spec. + See test_core.testHTTPMethods. +3. Max request header and body settings do not work with Apache. +4. Apache replaces status "reason phrases" automatically. For example, + CherryPy may set "304 Not modified" but Apache will write out + "304 Not Modified" (capital "M"). +5. Apache does not allow custom error codes as per the spec. +6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the + Request-URI too early. +7. mod_python will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. Apache will output a "Content-Length: 0" response header even if there's + no response entity body. This isn't really a bug; it just differs from + the CherryPy default. +""" + +import os +import re + +import cherrypy +from cherrypy.process import servers +from cherrypy.test import helper + +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +def read_process(cmd, args=''): + pipein, pipeout = os.popen4('%s %s' % (cmd, args)) + try: + firstline = pipeout.readline() + if (re.search(r'(not recognized|No such file|not found)', firstline, + re.IGNORECASE)): + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +APACHE_PATH = 'apache2ctl' +CONF_PATH = 'fastcgi.conf' + +conf_fastcgi = """ +# Apache2 server conf file for testing CherryPy with mod_fastcgi. +# fumanchu: I had to hard-code paths due to crazy Debian layouts :( +ServerRoot /usr/lib/apache2 +User #1000 +ErrorLog %(root)s/mod_fastcgi.error.log + +DocumentRoot "%(root)s" +ServerName 127.0.0.1 +Listen %(port)s +LoadModule fastcgi_module modules/mod_fastcgi.so +LoadModule rewrite_module modules/mod_rewrite.so + +Options +ExecCGI +SetHandler fastcgi-script +RewriteEngine On +RewriteRule ^(.*)$ /fastcgi.pyc [L] +FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000 +""" + + +def erase_script_name(environ, start_response): + environ['SCRIPT_NAME'] = '' + return cherrypy.tree(environ, start_response) + + +class ModFCGISupervisor(helper.LocalWSGISupervisor): + + httpserver_class = 'cherrypy.process.servers.FlupFCGIServer' + using_apache = True + using_wsgi = True + template = conf_fastcgi + + def __str__(self): + return 'FCGI Server on %s:%s' % (self.host, self.port) + + def start(self, modulename): + cherrypy.server.httpserver = servers.FlupFCGIServer( + application=erase_script_name, bindAddress=('127.0.0.1', 4000)) + cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000) + cherrypy.server.socket_port = 4000 + # For FCGI, we both start apache... + self.start_apache() + # ...and our local server + cherrypy.engine.start() + self.sync_apps() + + def start_apache(self): + fcgiconf = CONF_PATH + if not os.path.isabs(fcgiconf): + fcgiconf = os.path.join(curdir, fcgiconf) + + # Write the Apache conf file. + f = open(fcgiconf, 'wb') + try: + server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1] + output = self.template % {'port': self.port, 'root': curdir, + 'server': server} + output = output.replace('\r\n', '\n') + f.write(output) + finally: + f.close() + + result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf) + if result: + print(result) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, '-k stop') + helper.LocalWSGISupervisor.stop(self) + + def sync_apps(self): + cherrypy.server.httpserver.fcgiserver.application = self.get_app( + erase_script_name) diff --git a/resources/lib/cherrypy/test/modfcgid.py b/resources/lib/cherrypy/test/modfcgid.py new file mode 100644 index 0000000..d101bd6 --- /dev/null +++ b/resources/lib/cherrypy/test/modfcgid.py @@ -0,0 +1,124 @@ +"""Wrapper for mod_fcgid, for use as a CherryPy HTTP server when testing. + +To autostart fcgid, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl", "apache2ctl", +or "httpd"--create a symlink to them if needed. + +You'll also need the WSGIServer from flup.servers. +See http://projects.amor.org/misc/wiki/ModPythonGateway + + +KNOWN BUGS +========== + +1. Apache processes Range headers automatically; CherryPy's truncated + output is then truncated again by Apache. See test_core.testRanges. + This was worked around in http://www.cherrypy.org/changeset/1319. +2. Apache does not allow custom HTTP methods like CONNECT as per the spec. + See test_core.testHTTPMethods. +3. Max request header and body settings do not work with Apache. +4. Apache replaces status "reason phrases" automatically. For example, + CherryPy may set "304 Not modified" but Apache will write out + "304 Not Modified" (capital "M"). +5. Apache does not allow custom error codes as per the spec. +6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the + Request-URI too early. +7. mod_python will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. Apache will output a "Content-Length: 0" response header even if there's + no response entity body. This isn't really a bug; it just differs from + the CherryPy default. +""" + +import os +import re + +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy.process import servers +from cherrypy.test import helper + +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +def read_process(cmd, args=''): + pipein, pipeout = os.popen4('%s %s' % (cmd, args)) + try: + firstline = pipeout.readline() + if (re.search(r'(not recognized|No such file|not found)', firstline, + re.IGNORECASE)): + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +APACHE_PATH = 'httpd' +CONF_PATH = 'fcgi.conf' + +conf_fcgid = """ +# Apache2 server conf file for testing CherryPy with mod_fcgid. + +DocumentRoot "%(root)s" +ServerName 127.0.0.1 +Listen %(port)s +LoadModule fastcgi_module modules/mod_fastcgi.dll +LoadModule rewrite_module modules/mod_rewrite.so + +Options ExecCGI +SetHandler fastcgi-script +RewriteEngine On +RewriteRule ^(.*)$ /fastcgi.pyc [L] +FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000 +""" + + +class ModFCGISupervisor(helper.LocalSupervisor): + + using_apache = True + using_wsgi = True + template = conf_fcgid + + def __str__(self): + return 'FCGI Server on %s:%s' % (self.host, self.port) + + def start(self, modulename): + cherrypy.server.httpserver = servers.FlupFCGIServer( + application=cherrypy.tree, bindAddress=('127.0.0.1', 4000)) + cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000) + # For FCGI, we both start apache... + self.start_apache() + # ...and our local server + helper.LocalServer.start(self, modulename) + + def start_apache(self): + fcgiconf = CONF_PATH + if not os.path.isabs(fcgiconf): + fcgiconf = os.path.join(curdir, fcgiconf) + + # Write the Apache conf file. + f = open(fcgiconf, 'wb') + try: + server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1] + output = self.template % {'port': self.port, 'root': curdir, + 'server': server} + output = ntob(output.replace('\r\n', '\n')) + f.write(output) + finally: + f.close() + + result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf) + if result: + print(result) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, '-k stop') + helper.LocalServer.stop(self) + + def sync_apps(self): + cherrypy.server.httpserver.fcgiserver.application = self.get_app() diff --git a/resources/lib/cherrypy/test/modpy.py b/resources/lib/cherrypy/test/modpy.py new file mode 100644 index 0000000..7c288d2 --- /dev/null +++ b/resources/lib/cherrypy/test/modpy.py @@ -0,0 +1,164 @@ +"""Wrapper for mod_python, for use as a CherryPy HTTP server when testing. + +To autostart modpython, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl" or "apache2ctl"-- +create a symlink to them if needed. + +If you wish to test the WSGI interface instead of our _cpmodpy interface, +you also need the 'modpython_gateway' module at: +http://projects.amor.org/misc/wiki/ModPythonGateway + + +KNOWN BUGS +========== + +1. Apache processes Range headers automatically; CherryPy's truncated + output is then truncated again by Apache. See test_core.testRanges. + This was worked around in http://www.cherrypy.org/changeset/1319. +2. Apache does not allow custom HTTP methods like CONNECT as per the spec. + See test_core.testHTTPMethods. +3. Max request header and body settings do not work with Apache. +4. Apache replaces status "reason phrases" automatically. For example, + CherryPy may set "304 Not modified" but Apache will write out + "304 Not Modified" (capital "M"). +5. Apache does not allow custom error codes as per the spec. +6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the + Request-URI too early. +7. mod_python will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. Apache will output a "Content-Length: 0" response header even if there's + no response entity body. This isn't really a bug; it just differs from + the CherryPy default. +""" + +import os +import re + +import cherrypy +from cherrypy.test import helper + +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +def read_process(cmd, args=''): + pipein, pipeout = os.popen4('%s %s' % (cmd, args)) + try: + firstline = pipeout.readline() + if (re.search(r'(not recognized|No such file|not found)', firstline, + re.IGNORECASE)): + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +APACHE_PATH = 'httpd' +CONF_PATH = 'test_mp.conf' + +conf_modpython_gateway = """ +# Apache2 server conf file for testing CherryPy with modpython_gateway. + +ServerName 127.0.0.1 +DocumentRoot "/" +Listen %(port)s +LoadModule python_module modules/mod_python.so + +SetHandler python-program +PythonFixupHandler cherrypy.test.modpy::wsgisetup +PythonOption testmod %(modulename)s +PythonHandler modpython_gateway::handler +PythonOption wsgi.application cherrypy::tree +PythonOption socket_host %(host)s +PythonDebug On +""" + +conf_cpmodpy = """ +# Apache2 server conf file for testing CherryPy with _cpmodpy. + +ServerName 127.0.0.1 +DocumentRoot "/" +Listen %(port)s +LoadModule python_module modules/mod_python.so + +SetHandler python-program +PythonFixupHandler cherrypy.test.modpy::cpmodpysetup +PythonHandler cherrypy._cpmodpy::handler +PythonOption cherrypy.setup cherrypy.test.%(modulename)s::setup_server +PythonOption socket_host %(host)s +PythonDebug On +""" + + +class ModPythonSupervisor(helper.Supervisor): + + using_apache = True + using_wsgi = False + template = None + + def __str__(self): + return 'ModPython Server on %s:%s' % (self.host, self.port) + + def start(self, modulename): + mpconf = CONF_PATH + if not os.path.isabs(mpconf): + mpconf = os.path.join(curdir, mpconf) + + f = open(mpconf, 'wb') + try: + f.write(self.template % + {'port': self.port, 'modulename': modulename, + 'host': self.host}) + finally: + f.close() + + result = read_process(APACHE_PATH, '-k start -f %s' % mpconf) + if result: + print(result) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, '-k stop') + + +loaded = False + + +def wsgisetup(req): + global loaded + if not loaded: + loaded = True + options = req.get_options() + + cherrypy.config.update({ + 'log.error_file': os.path.join(curdir, 'test.log'), + 'environment': 'test_suite', + 'server.socket_host': options['socket_host'], + }) + + modname = options['testmod'] + mod = __import__(modname, globals(), locals(), ['']) + mod.setup_server() + + cherrypy.server.unsubscribe() + cherrypy.engine.start() + from mod_python import apache + return apache.OK + + +def cpmodpysetup(req): + global loaded + if not loaded: + loaded = True + options = req.get_options() + + cherrypy.config.update({ + 'log.error_file': os.path.join(curdir, 'test.log'), + 'environment': 'test_suite', + 'server.socket_host': options['socket_host'], + }) + from mod_python import apache + return apache.OK diff --git a/resources/lib/cherrypy/test/modwsgi.py b/resources/lib/cherrypy/test/modwsgi.py new file mode 100644 index 0000000..f558e22 --- /dev/null +++ b/resources/lib/cherrypy/test/modwsgi.py @@ -0,0 +1,154 @@ +"""Wrapper for mod_wsgi, for use as a CherryPy HTTP server. + +To autostart modwsgi, the "apache" executable or script must be +on your system path, or you must override the global APACHE_PATH. +On some platforms, "apache" may be called "apachectl" or "apache2ctl"-- +create a symlink to them if needed. + + +KNOWN BUGS +========== + +##1. Apache processes Range headers automatically; CherryPy's truncated +## output is then truncated again by Apache. See test_core.testRanges. +## This was worked around in http://www.cherrypy.org/changeset/1319. +2. Apache does not allow custom HTTP methods like CONNECT as per the spec. + See test_core.testHTTPMethods. +3. Max request header and body settings do not work with Apache. +##4. Apache replaces status "reason phrases" automatically. For example, +## CherryPy may set "304 Not modified" but Apache will write out +## "304 Not Modified" (capital "M"). +##5. Apache does not allow custom error codes as per the spec. +##6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the +## Request-URI too early. +7. mod_wsgi will not read request bodies which use the "chunked" + transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block + instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and + mod_python's requestobject.c). +8. When responding with 204 No Content, mod_wsgi adds a Content-Length + header for you. +9. When an error is raised, mod_wsgi has no facility for printing a + traceback as the response content (it's sent to the Apache log instead). +10. Startup and shutdown of Apache when running mod_wsgi seems slow. +""" + +import os +import re +import sys +import time + +import portend + +from cheroot.test import webtest + +import cherrypy +from cherrypy.test import helper + +curdir = os.path.abspath(os.path.dirname(__file__)) + + +def read_process(cmd, args=''): + pipein, pipeout = os.popen4('%s %s' % (cmd, args)) + try: + firstline = pipeout.readline() + if (re.search(r'(not recognized|No such file|not found)', firstline, + re.IGNORECASE)): + raise IOError('%s must be on your system path.' % cmd) + output = firstline + pipeout.read() + finally: + pipeout.close() + return output + + +if sys.platform == 'win32': + APACHE_PATH = 'httpd' +else: + APACHE_PATH = 'apache' + +CONF_PATH = 'test_mw.conf' + +conf_modwsgi = r""" +# Apache2 server conf file for testing CherryPy with modpython_gateway. + +ServerName 127.0.0.1 +DocumentRoot "/" +Listen %(port)s + +AllowEncodedSlashes On +LoadModule rewrite_module modules/mod_rewrite.so +RewriteEngine on +RewriteMap escaping int:escape + +LoadModule log_config_module modules/mod_log_config.so +LogFormat "%%h %%l %%u %%t \"%%r\" %%>s %%b \"%%{Referer}i\" \"%%{User-agent}i\"" combined +CustomLog "%(curdir)s/apache.access.log" combined +ErrorLog "%(curdir)s/apache.error.log" +LogLevel debug + +LoadModule wsgi_module modules/mod_wsgi.so +LoadModule env_module modules/mod_env.so + +WSGIScriptAlias / "%(curdir)s/modwsgi.py" +SetEnv testmod %(testmod)s +""" # noqa E501 + + +class ModWSGISupervisor(helper.Supervisor): + + """Server Controller for ModWSGI and CherryPy.""" + + using_apache = True + using_wsgi = True + template = conf_modwsgi + + def __str__(self): + return 'ModWSGI Server on %s:%s' % (self.host, self.port) + + def start(self, modulename): + mpconf = CONF_PATH + if not os.path.isabs(mpconf): + mpconf = os.path.join(curdir, mpconf) + + f = open(mpconf, 'wb') + try: + output = (self.template % + {'port': self.port, 'testmod': modulename, + 'curdir': curdir}) + f.write(output) + finally: + f.close() + + result = read_process(APACHE_PATH, '-k start -f %s' % mpconf) + if result: + print(result) + + # Make a request so mod_wsgi starts up our app. + # If we don't, concurrent initial requests will 404. + portend.occupied('127.0.0.1', self.port, timeout=5) + webtest.openURL('/ihopetheresnodefault', port=self.port) + time.sleep(1) + + def stop(self): + """Gracefully shutdown a server that is serving forever.""" + read_process(APACHE_PATH, '-k stop') + + +loaded = False + + +def application(environ, start_response): + global loaded + if not loaded: + loaded = True + modname = 'cherrypy.test.' + environ['testmod'] + mod = __import__(modname, globals(), locals(), ['']) + mod.setup_server() + + cherrypy.config.update({ + 'log.error_file': os.path.join(curdir, 'test.error.log'), + 'log.access_file': os.path.join(curdir, 'test.access.log'), + 'environment': 'test_suite', + 'engine.SIGHUP': None, + 'engine.SIGTERM': None, + }) + return cherrypy.tree(environ, start_response) diff --git a/resources/lib/cherrypy/test/sessiondemo.py b/resources/lib/cherrypy/test/sessiondemo.py new file mode 100644 index 0000000..8226c1b --- /dev/null +++ b/resources/lib/cherrypy/test/sessiondemo.py @@ -0,0 +1,161 @@ +#!/usr/bin/python +"""A session demonstration app.""" + +import calendar +from datetime import datetime +import sys + +import six + +import cherrypy +from cherrypy.lib import sessions + + +page = """ + + + + + + + +

Session Demo

+

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

+

Index | Expire | Regenerate

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

%(changemsg)s

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

Session Data%(sessiondata)s
Server Time%(servertime)s (Unix time: %(serverunixtime)s)
Browser Time 
Cherrypy Version:%(cpversion)s
Python Version:%(pyversion)s
+ +""" # noqa E501 + + +class Root(object): + + def page(self): + changemsg = [] + if cherrypy.session.id != cherrypy.session.originalid: + if cherrypy.session.originalid is None: + changemsg.append( + 'Created new session because no session id was given.') + if cherrypy.session.missing: + changemsg.append( + 'Created new session due to missing ' + '(expired or malicious) session.') + if cherrypy.session.regenerated: + changemsg.append('Application generated a new session.') + + try: + expires = cherrypy.response.cookie['session_id']['expires'] + except KeyError: + expires = '' + + return page % { + 'sessionid': cherrypy.session.id, + 'changemsg': '
'.join(changemsg), + 'respcookie': cherrypy.response.cookie.output(), + 'reqcookie': cherrypy.request.cookie.output(), + 'sessiondata': list(six.iteritems(cherrypy.session)), + 'servertime': ( + datetime.utcnow().strftime('%Y/%m/%d %H:%M') + ' UTC' + ), + 'serverunixtime': calendar.timegm(datetime.utcnow().timetuple()), + 'cpversion': cherrypy.__version__, + 'pyversion': sys.version, + 'expires': expires, + } + + @cherrypy.expose + def index(self): + # Must modify data or the session will not be saved. + cherrypy.session['color'] = 'green' + return self.page() + + @cherrypy.expose + def expire(self): + sessions.expire() + return self.page() + + @cherrypy.expose + def regen(self): + cherrypy.session.regenerate() + # Must modify data or the session will not be saved. + cherrypy.session['color'] = 'yellow' + return self.page() + + +if __name__ == '__main__': + cherrypy.config.update({ + # 'environment': 'production', + 'log.screen': True, + 'tools.sessions.on': True, + }) + cherrypy.quickstart(Root()) diff --git a/resources/lib/cherrypy/test/static/404.html b/resources/lib/cherrypy/test/static/404.html new file mode 100644 index 0000000..01b17b0 --- /dev/null +++ b/resources/lib/cherrypy/test/static/404.html @@ -0,0 +1,5 @@ + + +

I couldn't find that thing you were looking for!

+ + diff --git a/resources/lib/cherrypy/test/static/dirback.jpg b/resources/lib/cherrypy/test/static/dirback.jpg new file mode 100644 index 0000000000000000000000000000000000000000..80403dc227c19b9192158420f5288121e7f2669a GIT binary patch literal 16585 zcmbt*XHZjJ)b2?k2_^L2jDScNBy`k7T0p=M0wRb+K@7cviX{oX3JM58C3F%%kX|fw z5dlLN5D*kZq^Jnkxp}{D=Ki>U@6JpnIg{CEPS#%Kd7icQ-|W8w1Z^y>EddY+06?4< zus;i!0bE@0|F5;*2?!xTykIW~AOwJf0I(3S-viJ900R2&M*nw#zz_h6;9UD}{oktq z00u#zT!H`y0)}wG!7$D@Fz1&b&OQfqgcY!mqbD6Q2^TJggi;Ide4!Q9b&W1TMHF?- z9(Rs-eshfJ9$OUpe|F?Noe+3`2H=DI&nF?^1n~Vu^T3ZI_>QeL%Ggu5I;IpTZh4}j zErN>A74Ka=3t6Y@@U`te4t|Oc`i2Y_T}~n>kkZEjrY%(L_%GbVO&pJvv{SDqa4YBK z5&2%mC`qp}RUW)|Ubt6?15(cfiRZQa;fbHlOlr-)`MS7++VnzaFzA)aK&`Fen9kDQ z+kcsb)T3c1K7A#*YP<|j4a$Eh>3NhV-oxY(Ee%wkoA=S_|q6=&SAAFi>&SkYW| zrdLsciEwk?#yPza$wULmwArF;OOty)zC5``q!HsK=ImsZETR_7=vK zVTOAc<6>7qpKTW_6uj=Ui3bn8qQ_FrB%!CQVbdPERX=rF^iU0(c-cn;P zcU`#>PCOtGMofiNqAW9Q%R(<9&d5R#zmrxKX~;$cBEJFm)5*= zw1-rLfYxcWDUlB`;TZh{)o*P&QW>O-&`(Q*3TUcAS=rY`3(Y#z@YAnx1jm^m>D$4@ zrP@$Idzo|44~BH2CT`_};t)v0L(f*obO7mYeHOO@3|>%3pRgldM3UdPl{Nf6df814 zG#pYUED(yP6i8U0E*{%muM+Ly>!g{gQ3b$eshMwC4tQ_Ibyn78`K@e?#C@RkZK=gR z5LyRf=~zlg^+2Q41g{vT@%eC_q~P*kFG0{W*;?RqN)fq9Wafu+4$_pZKI3UYJM?J9 zSQVSJEmU<`i+7emFb-^G~Of*#XN-M*um9BSD8Hdrd} znfru>)1xT;WJ3>$1)f6hyC-q=;ce#cV#-YDmxz)wvX(nV!>*`IsoSSmg-!9&wlD5D z7(KO{Ew2va@z<4UKg}~UvFkwKR%?Zj5la2(<`-=QrQc#@vnI6j7F{Lvy2#Tb!jX`( zz!?i$Q&a~(**v(Jo(+SIU)0+=o2DxhU*|;1zrWeJOAs2n5?o5lw4_BGUyB2_~1>ewZCxt5iA>8QNXejy) z3k)TkrguJNTd%~IPhY&F)X5|O#}$~lc#{Mtq@*d0O98|Xop3sp%}A;=AXapzgdTd% zB$ahX$1il^k4)!Dy=9`LL3iFuM|oRti{Q7vGi_2~Q9X8+VrE4T`t^yh2mJ@ra%&e& z-e!;EMdw==QH1V->21=5=Ecaj;@02dv z^9xTjdGgm(jd3GS>#kDzm*yNb&?XT=)%CbSkN|DSi$Wm9S3L#lp|*M1=jiF|6{BZ5 zs!%~&IQV+jgsgQ1>bE+djwHi8N6s4ZtYQRMQ-6y$Aj)=d&uusgXJg2Puhwn)@;$9= z1rbh+4UBTn*h#8eh}BtK@CNC-6YWltdjFK&tEy;=yrt((w{3V z^Qt2diQh1tkJ5}$D3i8TeqVeP;~1W#@GUkBFcPSoEX4zYp*<-!Xih2NvQwvY{=#jqoJY&+4iU1yuLtAv+yQ>PcJyI_O?JWL7f3hlex4Ai55t=!1IStU{vP?;rRor8EzzO2rB8r1Rh!S-efXpjd(ZS zIPdix-lNrN*__yYHJ^; z12qR82GNoIE({s{-G|gK`jnTk{L+V0QY@@boKMs<5{17hI>+Fi3zCqiwxKdQ2d2Ac zzF(Piu*;C_v1nxpH0g~hCJ}H*XpSm@l1y;}C$uX1bmV0$th1CW6mr%Y1hEajMfp<` zk9)Cae=><+d-Z3^Z%q*r-rI^sPl(q9!&@bnFMq>TEGZfl8#X6rJwh7fj}VA{)QS+H z5N@?>ADt)H)J7Z(?z%yW*z>CrRBVLEaGt}QmV;rE$PmMrb_N2qL?h8VCAQl3E@ zO^(q?k}?ZkKSdfU9#5sx&{ zAyKr~3%{P%(tR=)4EvUuGfw>hRTuTVdUd#}NK(SUmMRy*`>~^194P4EaWbOYhMx%m zxD+1xGUj^~72L3nR?-mzJFiPmX5J49(pXjJ4{_`+FEo;~w=~N0lGVAt54?yX7Hrl= z=sgm&<-r1KVt%K!o4J6d&`JnsX#^6@a4$_eUFRUB?bWtXLEsy^^7EglZ@US-;EA7k zK^VNECsjL`c8x)@5Ij*=_%ylm>1`tPca`UT$n<|kWgrzod(mep>zzF+H`pdGVKanE z%S_HG8OyUYXhX`aPVW7{9D89Ab6;eTV@wC{1f7CI=;QrUGb3?l6b*%!WJIOoVqiiz z|DPz7p*cXML^4p|hy!ZW^d)K}1zV^_{s{gkFz}Js!SA7dGWUZZ$qGf4$3Q#eJ{~P8 zu|rx32qgXEQq$C)YhM3x1F`bxRr>6cr@picLoER_10~u)ag)zy1%a{3$cKIXr7>3N zEfICF+wK!^ABBu<!?SD)W>W?9k1Dh4Fx873ce1-@( zHWsS$wc%WBbH#P8Y zBiGEoC>DpH{KsA3PtlKim9k-M=XFG3@G0l^km9ipQNojBSwLb@g!F(~0w6 zD$O8EQRZTpb~7`_b(*E^_*~@XB{(1QN%uo^gML%l3pBsLM z&njvFB-aaEDJ-HJHK66r=mByI>L<}cQRv^Oa&zICO*(Z!GW!rQ4r&_KeQ&&OgrZ>BrUKOS%J2+yU_cqdqfT6Vymj@*=ADB~FdE zRh|{I6y^07hdA=cV`%BDFyjHXKo1}I)FIvSHm}C!5rJx(xO#ZlPKbu@EN?RJEv|DV z`jmUQ-QJ(_9O1mRX(Q)xDt+{OfPF?CFD$*^35Jx*;DB}*wSUL8|87S$GM}|laZ;CV zyei@43p+ez1&v`&y1p=MZqEoo{zHBg5dTIUpEO1Zs=8ju=AlGm@mG$Ozy!$E0-J)0 z>wY89PlI#Mve6o29!=Ha)04iZ_W>`-&Tz=vEWX1hjg{cYYWrnMgV2CUHXOrk43#2N zt}18VT4|-pzSj}|hhTf&KL<)A^0VzoBJ+-n36!q3;B?Bm5IWpm_i}#exRs z(0ztfR+2>^tdGPlBmPPle3UZQ3XP1#ZU?0#5ghmj!jLOpn2r}2gIb%i$KtHR_kr}w zVlDpK3`c&gUWpazVgm~q_h1i;nf%elAic?7xWnFAxrUn9!uC%tsMeQ=_*dv+AGyp; z{>;Ii{@VGXJ^o&A5`NZuWls4)l{(;govY5o{lPetCopBVmT6O+JNM^R3BQh`Y5}2N zh3$vh!#V+w_GQTxA(`sA$Y=7hYeW->*^hSge ze{Qn6c^izzY&2jFAL?*a;1j@|;;A35-%F38Qt+n1s9{;4SU$pDBtYjtH`-3shIUIp8yMy~J z+-^&t8g6DraGv)0HZ}mZh_&F1a!~G#A;@ z;!^5RLLq~4-u!sj#Ps;Om&VD#cxvj*LdsQwz&iD5PJH@S7W_X8{=~NJVBr+&@}X}` z9=H7PxrqYTocLj{pc{>66iYddcs$t(VRZXX^zaC7?Lsb)oU5(P5(jo^j4#nRSs%P} z%%-6F%NUr+Xp+da{6<_)qEy=4FXv?c!Pg5SY{fKOZuy}Y2qQS}1CI9aC2o(Nso|kALE+fW;>7iB%aq z#m{lQIi4J8xMCYM0~@7gzR0i!w(;B{CP$PGd3e9-ibTLuDB>S z814c!JeP=(RSEEt?V%yF;sdHC)K0%uKNTIz-EbID5$MOrx^FV`Z$eermS?x&tA@lI zGy1LXOl$Fs_ek=?%L$)d<*El&j?DgBRnhk!+d(m=YlZy!Ss?2D$-eCHijS0p!pt6i5=SPlxZZ{7XQ)u#`5fg0Pg-1YCJ_

9L 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 Sse&CiQ+RTS)_Y=q`u_k!0j`Vy literal 0 HcmV?d00001 diff --git a/resources/lib/cherrypy/test/static/index.html b/resources/lib/cherrypy/test/static/index.html new file mode 100644 index 0000000..a5c1966 --- /dev/null +++ b/resources/lib/cherrypy/test/static/index.html @@ -0,0 +1 @@ +Hello, world diff --git a/resources/lib/cherrypy/test/style.css b/resources/lib/cherrypy/test/style.css new file mode 100644 index 0000000..b266e93 --- /dev/null +++ b/resources/lib/cherrypy/test/style.css @@ -0,0 +1 @@ +Dummy stylesheet diff --git a/resources/lib/cherrypy/test/test.pem b/resources/lib/cherrypy/test/test.pem new file mode 100644 index 0000000..47a4704 --- /dev/null +++ b/resources/lib/cherrypy/test/test.pem @@ -0,0 +1,38 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQDBKo554mzIMY+AByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZ +R9L4WtImEew05FY3Izerfm3MN3+MC0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Kn +da+O6xldVSosu8Ev3z9VZ94iC/ZgKzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQAB +AoGAWOCF0ZrWxn3XMucWq2LNwPKqlvVGwbIwX3cDmX22zmnM4Fy6arXbYh4XlyCj +9+ofqRrxIFz5k/7tFriTmZ0xag5+Jdx+Kwg0/twiP7XCNKipFogwe1Hznw8OFAoT +enKBdj2+/n2o0Bvo/tDB59m9L/538d46JGQUmJlzMyqYikECQQDyoq+8CtMNvE18 +8VgHcR/KtApxWAjj4HpaHYL637ATjThetUZkW92mgDgowyplthusxdNqhHWyv7E8 +tWNdYErZAkEAy85ShTR0M5aWmrE7o0r0SpWInAkNBH9aXQRRARFYsdBtNfRu6I0i +0lvU9wiu3eF57FMEC86yViZ5UBnQfTu7vQJAVesj/Zt7pwaCDfdMa740OsxMUlyR +MVhhGx4OLpYdPJ8qUecxGQKq13XZ7R1HGyNEY4bd2X80Smq08UFuATfC6QJAH8UB +yBHtKz2GLIcELOg6PIYizW/7v3+6rlVF60yw7sb2vzpjL40QqIn4IKoR2DSVtOkb +8FtAIX3N21aq0VrGYQJBAIPiaEc2AZ8Bq2GC4F3wOz/BxJ/izvnkiotR12QK4fh5 +yjZMhTjWCas5zwHR5PDjlD88AWGDMsZ1PicD4348xJQ= +-----END RSA PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +MIIDxTCCAy6gAwIBAgIJAI18BD7eQxlGMA0GCSqGSIb3DQEBBAUAMIGeMQswCQYD +VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU2FuIERpZWdv +MRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0MREwDwYDVQQLEwhkZXYtdGVzdDEW +MBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4GCSqGSIb3DQEJARYRcmVtaUBjaGVy +cnlweS5vcmcwHhcNMDYwOTA5MTkyMDIwWhcNMzQwMTI0MTkyMDIwWjCBnjELMAkG +A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVNhbiBEaWVn +bzEZMBcGA1UEChMQQ2hlcnJ5UHkgUHJvamVjdDERMA8GA1UECxMIZGV2LXRlc3Qx +FjAUBgNVBAMTDUNoZXJyeVB5IFRlYW0xIDAeBgkqhkiG9w0BCQEWEXJlbWlAY2hl +cnJ5cHkub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBKo554mzIMY+A +ByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZR9L4WtImEew05FY3Izerfm3MN3+M +C0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Knda+O6xldVSosu8Ev3z9VZ94iC/Zg +KzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQABo4IBBzCCAQMwHQYDVR0OBBYEFDIQ +2feb71tVZCWpU0qJ/Tw+wdtoMIHTBgNVHSMEgcswgciAFDIQ2feb71tVZCWpU0qJ +/Tw+wdtooYGkpIGhMIGeMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p +YTESMBAGA1UEBxMJU2FuIERpZWdvMRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0 +MREwDwYDVQQLEwhkZXYtdGVzdDEWMBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4G +CSqGSIb3DQEJARYRcmVtaUBjaGVycnlweS5vcmeCCQCNfAQ+3kMZRjAMBgNVHRME +BTADAQH/MA0GCSqGSIb3DQEBBAUAA4GBAL7AAQz7IePV48ZTAFHKr88ntPALsL5S +8vHCZPNMevNkLTj3DYUw2BcnENxMjm1kou2F2BkvheBPNZKIhc6z4hAml3ed1xa2 +D7w6e6OTcstdK/+KrPDDHeOP1dhMWNs2JE1bNlfF1LiXzYKSXpe88eCKjCXsCT/T +NluCaWQys3MS +-----END CERTIFICATE----- diff --git a/resources/lib/cherrypy/test/test_auth_basic.py b/resources/lib/cherrypy/test/test_auth_basic.py new file mode 100644 index 0000000..d7e69a9 --- /dev/null +++ b/resources/lib/cherrypy/test/test_auth_basic.py @@ -0,0 +1,135 @@ +# This file is part of CherryPy +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 + +from hashlib import md5 + +import cherrypy +from cherrypy._cpcompat import ntob +from cherrypy.lib import auth_basic +from cherrypy.test import helper + + +class BasicAuthTest(helper.CPWebCase): + + @staticmethod + def setup_server(): + class Root: + + @cherrypy.expose + def index(self): + return 'This is public.' + + class BasicProtected: + + @cherrypy.expose + def index(self): + return "Hello %s, you've been authorized." % ( + cherrypy.request.login) + + class BasicProtected2: + + @cherrypy.expose + def index(self): + return "Hello %s, you've been authorized." % ( + cherrypy.request.login) + + class BasicProtected2_u: + + @cherrypy.expose + def index(self): + return "Hello %s, you've been authorized." % ( + cherrypy.request.login) + + userpassdict = {'xuser': 'xpassword'} + userhashdict = {'xuser': md5(b'xpassword').hexdigest()} + userhashdict_u = {'xюзер': md5(ntob('їжа', 'utf-8')).hexdigest()} + + def checkpasshash(realm, user, password): + p = userhashdict.get(user) + return p and p == md5(ntob(password)).hexdigest() or False + + def checkpasshash_u(realm, user, password): + p = userhashdict_u.get(user) + return p and p == md5(ntob(password, 'utf-8')).hexdigest() or False + + basic_checkpassword_dict = auth_basic.checkpassword_dict(userpassdict) + conf = { + '/basic': { + 'tools.auth_basic.on': True, + 'tools.auth_basic.realm': 'wonderland', + 'tools.auth_basic.checkpassword': basic_checkpassword_dict + }, + '/basic2': { + 'tools.auth_basic.on': True, + 'tools.auth_basic.realm': 'wonderland', + 'tools.auth_basic.checkpassword': checkpasshash, + 'tools.auth_basic.accept_charset': 'ISO-8859-1', + }, + '/basic2_u': { + 'tools.auth_basic.on': True, + 'tools.auth_basic.realm': 'wonderland', + 'tools.auth_basic.checkpassword': checkpasshash_u, + 'tools.auth_basic.accept_charset': 'UTF-8', + }, + } + + root = Root() + root.basic = BasicProtected() + root.basic2 = BasicProtected2() + root.basic2_u = BasicProtected2_u() + cherrypy.tree.mount(root, config=conf) + + def testPublic(self): + self.getPage('/') + self.assertStatus('200 OK') + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + self.assertBody('This is public.') + + def testBasic(self): + self.getPage('/basic/') + self.assertStatus(401) + self.assertHeader( + 'WWW-Authenticate', + 'Basic realm="wonderland", charset="UTF-8"' + ) + + self.getPage('/basic/', + [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) + self.assertStatus(401) + + self.getPage('/basic/', + [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')]) + self.assertStatus('200 OK') + self.assertBody("Hello xuser, you've been authorized.") + + def testBasic2(self): + self.getPage('/basic2/') + self.assertStatus(401) + self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"') + + self.getPage('/basic2/', + [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) + self.assertStatus(401) + + self.getPage('/basic2/', + [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')]) + self.assertStatus('200 OK') + self.assertBody("Hello xuser, you've been authorized.") + + def testBasic2_u(self): + self.getPage('/basic2_u/') + self.assertStatus(401) + self.assertHeader( + 'WWW-Authenticate', + 'Basic realm="wonderland", charset="UTF-8"' + ) + + self.getPage('/basic2_u/', + [('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbRgw==')]) + self.assertStatus(401) + + self.getPage('/basic2_u/', + [('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbQsA==')]) + self.assertStatus('200 OK') + self.assertBody("Hello xюзер, you've been authorized.") diff --git a/resources/lib/cherrypy/test/test_auth_digest.py b/resources/lib/cherrypy/test/test_auth_digest.py new file mode 100644 index 0000000..512e39a --- /dev/null +++ b/resources/lib/cherrypy/test/test_auth_digest.py @@ -0,0 +1,134 @@ +# This file is part of CherryPy +# -*- coding: utf-8 -*- +# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 + +import six + + +import cherrypy +from cherrypy.lib import auth_digest +from cherrypy._cpcompat import ntob + +from cherrypy.test import helper + + +def _fetch_users(): + return {'test': 'test', '☃йюзер': 'їпароль'} + + +get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(_fetch_users()) + + +class DigestAuthTest(helper.CPWebCase): + + @staticmethod + def setup_server(): + class Root: + + @cherrypy.expose + def index(self): + return 'This is public.' + + class DigestProtected: + + @cherrypy.expose + def index(self, *args, **kwargs): + return "Hello %s, you've been authorized." % ( + cherrypy.request.login) + + conf = {'/digest': {'tools.auth_digest.on': True, + 'tools.auth_digest.realm': 'localhost', + 'tools.auth_digest.get_ha1': get_ha1, + 'tools.auth_digest.key': 'a565c27146791cfb', + 'tools.auth_digest.debug': True, + 'tools.auth_digest.accept_charset': 'UTF-8'}} + + root = Root() + root.digest = DigestProtected() + cherrypy.tree.mount(root, config=conf) + + def testPublic(self): + self.getPage('/') + assert self.status == '200 OK' + self.assertHeader('Content-Type', 'text/html;charset=utf-8') + assert self.body == b'This is public.' + + def _test_parametric_digest(self, username, realm): + test_uri = '/digest/?@/=%2F%40&%f0%9f%99%88=path' + + self.getPage(test_uri) + assert self.status_code == 401 + + msg = 'Digest authentification scheme was not found' + www_auth_digest = tuple(filter( + lambda kv: kv[0].lower() == 'www-authenticate' + and kv[1].startswith('Digest '), + self.headers, + )) + assert len(www_auth_digest) == 1, msg + + items = www_auth_digest[0][-1][7:].split(', ') + tokens = {} + for item in items: + key, value = item.split('=') + tokens[key.lower()] = value + + assert tokens['realm'] == '"localhost"' + assert tokens['algorithm'] == '"MD5"' + assert tokens['qop'] == '"auth"' + assert tokens['charset'] == '"UTF-8"' + + nonce = tokens['nonce'].strip('"') + + # Test user agent response with a wrong value for 'realm' + base_auth = ('Digest username="%s", ' + 'realm="%s", ' + 'nonce="%s", ' + 'uri="%s", ' + 'algorithm=MD5, ' + 'response="%s", ' + 'qop=auth, ' + 'nc=%s, ' + 'cnonce="1522e61005789929"') + + encoded_user = username + if six.PY3: + encoded_user = encoded_user.encode('utf-8') + encoded_user = encoded_user.decode('latin1') + auth_header = base_auth % ( + encoded_user, realm, nonce, test_uri, + '11111111111111111111111111111111', '00000001', + ) + auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET') + # calculate the response digest + ha1 = get_ha1(auth.realm, auth.username) + response = auth.request_digest(ha1) + auth_header = base_auth % ( + encoded_user, realm, nonce, test_uri, + response, '00000001', + ) + self.getPage(test_uri, [('Authorization', auth_header)]) + + def test_wrong_realm(self): + # send response with correct response digest, but wrong realm + self._test_parametric_digest(username='test', realm='wrong realm') + assert self.status_code == 401 + + def test_ascii_user(self): + self._test_parametric_digest(username='test', realm='localhost') + assert self.status == '200 OK' + assert self.body == b"Hello test, you've been authorized." + + def test_unicode_user(self): + self._test_parametric_digest(username='☃йюзер', realm='localhost') + assert self.status == '200 OK' + assert self.body == ntob( + "Hello ☃йюзер, you've been authorized.", 'utf-8', + ) + + def test_wrong_scheme(self): + basic_auth = { + 'Authorization': 'Basic foo:bar', + } + self.getPage('/digest/', headers=list(basic_auth.items())) + assert self.status_code == 401 diff --git a/resources/lib/cherrypy/test/test_bus.py b/resources/lib/cherrypy/test/test_bus.py new file mode 100644 index 0000000..6026b47 --- /dev/null +++ b/resources/lib/cherrypy/test/test_bus.py @@ -0,0 +1,274 @@ +import threading +import time +import unittest + +from cherrypy.process import wspbus + + +msg = 'Listener %d on channel %s: %s.' + + +class PublishSubscribeTests(unittest.TestCase): + + def get_listener(self, channel, index): + def listener(arg=None): + self.responses.append(msg % (index, channel, arg)) + return listener + + def test_builtin_channels(self): + b = wspbus.Bus() + + self.responses, expected = [], [] + + for channel in b.listeners: + for index, priority in enumerate([100, 50, 0, 51]): + b.subscribe(channel, + self.get_listener(channel, index), priority) + + for channel in b.listeners: + b.publish(channel) + expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)]) + b.publish(channel, arg=79347) + expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)]) + + self.assertEqual(self.responses, expected) + + def test_custom_channels(self): + b = wspbus.Bus() + + self.responses, expected = [], [] + + custom_listeners = ('hugh', 'louis', 'dewey') + for channel in custom_listeners: + for index, priority in enumerate([None, 10, 60, 40]): + b.subscribe(channel, + self.get_listener(channel, index), priority) + + for channel in custom_listeners: + b.publish(channel, 'ah so') + expected.extend([msg % (i, channel, 'ah so') + for i in (1, 3, 0, 2)]) + b.publish(channel) + expected.extend([msg % (i, channel, None) for i in (1, 3, 0, 2)]) + + self.assertEqual(self.responses, expected) + + def test_listener_errors(self): + b = wspbus.Bus() + + self.responses, expected = [], [] + channels = [c for c in b.listeners if c != 'log'] + + for channel in channels: + b.subscribe(channel, self.get_listener(channel, 1)) + # This will break since the lambda takes no args. + b.subscribe(channel, lambda: None, priority=20) + + for channel in channels: + self.assertRaises(wspbus.ChannelFailures, b.publish, channel, 123) + expected.append(msg % (1, channel, 123)) + + self.assertEqual(self.responses, expected) + + +class BusMethodTests(unittest.TestCase): + + def log(self, bus): + self._log_entries = [] + + def logit(msg, level): + self._log_entries.append(msg) + bus.subscribe('log', logit) + + def assertLog(self, entries): + self.assertEqual(self._log_entries, entries) + + def get_listener(self, channel, index): + def listener(arg=None): + self.responses.append(msg % (index, channel, arg)) + return listener + + def test_start(self): + b = wspbus.Bus() + self.log(b) + + self.responses = [] + num = 3 + for index in range(num): + b.subscribe('start', self.get_listener('start', index)) + + b.start() + try: + # The start method MUST call all 'start' listeners. + self.assertEqual( + set(self.responses), + set([msg % (i, 'start', None) for i in range(num)])) + # The start method MUST move the state to STARTED + # (or EXITING, if errors occur) + self.assertEqual(b.state, b.states.STARTED) + # The start method MUST log its states. + self.assertLog(['Bus STARTING', 'Bus STARTED']) + finally: + # Exit so the atexit handler doesn't complain. + b.exit() + + def test_stop(self): + b = wspbus.Bus() + self.log(b) + + self.responses = [] + num = 3 + for index in range(num): + b.subscribe('stop', self.get_listener('stop', index)) + + b.stop() + + # The stop method MUST call all 'stop' listeners. + self.assertEqual(set(self.responses), + set([msg % (i, 'stop', None) for i in range(num)])) + # The stop method MUST move the state to STOPPED + self.assertEqual(b.state, b.states.STOPPED) + # The stop method MUST log its states. + self.assertLog(['Bus STOPPING', 'Bus STOPPED']) + + def test_graceful(self): + b = wspbus.Bus() + self.log(b) + + self.responses = [] + num = 3 + for index in range(num): + b.subscribe('graceful', self.get_listener('graceful', index)) + + b.graceful() + + # The graceful method MUST call all 'graceful' listeners. + self.assertEqual( + set(self.responses), + set([msg % (i, 'graceful', None) for i in range(num)])) + # The graceful method MUST log its states. + self.assertLog(['Bus graceful']) + + def test_exit(self): + b = wspbus.Bus() + self.log(b) + + self.responses = [] + num = 3 + for index in range(num): + b.subscribe('stop', self.get_listener('stop', index)) + b.subscribe('exit', self.get_listener('exit', index)) + + b.exit() + + # The exit method MUST call all 'stop' listeners, + # and then all 'exit' listeners. + self.assertEqual(set(self.responses), + set([msg % (i, 'stop', None) for i in range(num)] + + [msg % (i, 'exit', None) for i in range(num)])) + # The exit method MUST move the state to EXITING + self.assertEqual(b.state, b.states.EXITING) + # The exit method MUST log its states. + self.assertLog( + ['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED']) + + def test_wait(self): + b = wspbus.Bus() + + def f(method): + time.sleep(0.2) + getattr(b, method)() + + for method, states in [('start', [b.states.STARTED]), + ('stop', [b.states.STOPPED]), + ('start', + [b.states.STARTING, b.states.STARTED]), + ('exit', [b.states.EXITING]), + ]: + threading.Thread(target=f, args=(method,)).start() + b.wait(states) + + # The wait method MUST wait for the given state(s). + if b.state not in states: + self.fail('State %r not in %r' % (b.state, states)) + + def test_block(self): + b = wspbus.Bus() + self.log(b) + + def f(): + time.sleep(0.2) + b.exit() + + def g(): + time.sleep(0.4) + threading.Thread(target=f).start() + threading.Thread(target=g).start() + threads = [t for t in threading.enumerate() if not t.daemon] + self.assertEqual(len(threads), 3) + + b.block() + + # The block method MUST wait for the EXITING state. + self.assertEqual(b.state, b.states.EXITING) + # The block method MUST wait for ALL non-main, non-daemon threads to + # finish. + threads = [t for t in threading.enumerate() if not t.daemon] + self.assertEqual(len(threads), 1) + # The last message will mention an indeterminable thread name; ignore + # it + self.assertEqual(self._log_entries[:-1], + ['Bus STOPPING', 'Bus STOPPED', + 'Bus EXITING', 'Bus EXITED', + 'Waiting for child threads to terminate...']) + + def test_start_with_callback(self): + b = wspbus.Bus() + self.log(b) + try: + events = [] + + def f(*args, **kwargs): + events.append(('f', args, kwargs)) + + def g(): + events.append('g') + b.subscribe('start', g) + b.start_with_callback(f, (1, 3, 5), {'foo': 'bar'}) + # Give wait() time to run f() + time.sleep(0.2) + + # The callback method MUST wait for the STARTED state. + self.assertEqual(b.state, b.states.STARTED) + # The callback method MUST run after all start methods. + self.assertEqual(events, ['g', ('f', (1, 3, 5), {'foo': 'bar'})]) + finally: + b.exit() + + def test_log(self): + b = wspbus.Bus() + self.log(b) + self.assertLog([]) + + # Try a normal message. + expected = [] + for msg in ["O mah darlin'"] * 3 + ['Clementiiiiiiiine']: + b.log(msg) + expected.append(msg) + self.assertLog(expected) + + # Try an error message + try: + foo + except NameError: + b.log('You are lost and gone forever', traceback=True) + lastmsg = self._log_entries[-1] + if 'Traceback' not in lastmsg or 'NameError' not in lastmsg: + self.fail('Last log message %r did not contain ' + 'the expected traceback.' % lastmsg) + else: + self.fail('NameError was not raised as expected.') + + +if __name__ == '__main__': + unittest.main() diff --git a/resources/lib/cherrypy/test/test_caching.py b/resources/lib/cherrypy/test/test_caching.py new file mode 100644 index 0000000..1a6ed4f --- /dev/null +++ b/resources/lib/cherrypy/test/test_caching.py @@ -0,0 +1,392 @@ +import datetime +from itertools import count +import os +import threading +import time + +from six.moves import range +from six.moves import urllib + +import pytest + +import cherrypy +from cherrypy.lib import httputil + +from cherrypy.test import helper + + +curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + +gif_bytes = ( + b'GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00' + b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' + b'\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;' +) + + +class CacheTest(helper.CPWebCase): + + @staticmethod + def setup_server(): + + @cherrypy.config(**{'tools.caching.on': True}) + class Root: + + def __init__(self): + self.counter = 0 + self.control_counter = 0 + self.longlock = threading.Lock() + + @cherrypy.expose + def index(self): + self.counter += 1 + msg = 'visit #%s' % self.counter + return msg + + @cherrypy.expose + def control(self): + self.control_counter += 1 + return 'visit #%s' % self.control_counter + + @cherrypy.expose + def a_gif(self): + cherrypy.response.headers[ + 'Last-Modified'] = httputil.HTTPDate() + return gif_bytes + + @cherrypy.expose + def long_process(self, seconds='1'): + try: + self.longlock.acquire() + time.sleep(float(seconds)) + finally: + self.longlock.release() + return 'success!' + + @cherrypy.expose + def clear_cache(self, path): + cherrypy._cache.store[cherrypy.request.base + path].clear() + + @cherrypy.config(**{ + 'tools.caching.on': True, + 'tools.response_headers.on': True, + 'tools.response_headers.headers': [ + ('Vary', 'Our-Varying-Header') + ], + }) + class VaryHeaderCachingServer(object): + + def __init__(self): + self.counter = count(1) + + @cherrypy.expose + def index(self): + return 'visit #%s' % next(self.counter) + + @cherrypy.config(**{ + 'tools.expires.on': True, + 'tools.expires.secs': 60, + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.root': curdir, + }) + class UnCached(object): + + @cherrypy.expose + @cherrypy.config(**{'tools.expires.secs': 0}) + def force(self): + cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' + self._cp_config['tools.expires.force'] = True + self._cp_config['tools.expires.secs'] = 0 + return 'being forceful' + + @cherrypy.expose + def dynamic(self): + cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' + cherrypy.response.headers['Cache-Control'] = 'private' + return 'D-d-d-dynamic!' + + @cherrypy.expose + def cacheable(self): + cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' + return "Hi, I'm cacheable." + + @cherrypy.expose + @cherrypy.config(**{'tools.expires.secs': 86400}) + def specific(self): + cherrypy.response.headers[ + 'Etag'] = 'need_this_to_make_me_cacheable' + return 'I am being specific' + + class Foo(object): + pass + + @cherrypy.expose + @cherrypy.config(**{'tools.expires.secs': Foo()}) + def wrongtype(self): + cherrypy.response.headers[ + 'Etag'] = 'need_this_to_make_me_cacheable' + return 'Woops' + + @cherrypy.config(**{ + 'tools.gzip.mime_types': ['text/*', 'image/*'], + 'tools.caching.on': True, + 'tools.staticdir.on': True, + 'tools.staticdir.dir': 'static', + 'tools.staticdir.root': curdir + }) + class GzipStaticCache(object): + pass + + cherrypy.tree.mount(Root()) + cherrypy.tree.mount(UnCached(), '/expires') + cherrypy.tree.mount(VaryHeaderCachingServer(), '/varying_headers') + cherrypy.tree.mount(GzipStaticCache(), '/gzip_static_cache') + cherrypy.config.update({'tools.gzip.on': True}) + + def testCaching(self): + elapsed = 0.0 + for trial in range(10): + self.getPage('/') + # The response should be the same every time, + # except for the Age response header. + self.assertBody('visit #1') + if trial != 0: + age = int(self.assertHeader('Age')) + self.assert_(age >= elapsed) + elapsed = age + + # POST, PUT, DELETE should not be cached. + self.getPage('/', method='POST') + self.assertBody('visit #2') + # Because gzip is turned on, the Vary header should always Vary for + # content-encoding + self.assertHeader('Vary', 'Accept-Encoding') + # The previous request should have invalidated the cache, + # so this request will recalc the response. + self.getPage('/', method='GET') + self.assertBody('visit #3') + # ...but this request should get the cached copy. + self.getPage('/', method='GET') + self.assertBody('visit #3') + self.getPage('/', method='DELETE') + self.assertBody('visit #4') + + # The previous request should have invalidated the cache, + # so this request will recalc the response. + self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')]) + self.assertHeader('Content-Encoding', 'gzip') + self.assertHeader('Vary') + self.assertEqual( + cherrypy.lib.encoding.decompress(self.body), b'visit #5') + + # Now check that a second request gets the gzip header and gzipped body + # This also tests a bug in 3.0 to 3.0.2 whereby the cached, gzipped + # response body was being gzipped a second time. + self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')]) + self.assertHeader('Content-Encoding', 'gzip') + self.assertEqual( + cherrypy.lib.encoding.decompress(self.body), b'visit #5') + + # Now check that a third request that doesn't accept gzip + # skips the cache (because the 'Vary' header denies it). + self.getPage('/', method='GET') + self.assertNoHeader('Content-Encoding') + self.assertBody('visit #6') + + def testVaryHeader(self): + self.getPage('/varying_headers/') + self.assertStatus('200 OK') + self.assertHeaderItemValue('Vary', 'Our-Varying-Header') + self.assertBody('visit #1') + + # Now check that different 'Vary'-fields don't evict each other. + # This test creates 2 requests with different 'Our-Varying-Header' + # and then tests if the first one still exists. + self.getPage('/varying_headers/', + headers=[('Our-Varying-Header', 'request 2')]) + self.assertStatus('200 OK') + self.assertBody('visit #2') + + self.getPage('/varying_headers/', + headers=[('Our-Varying-Header', 'request 2')]) + self.assertStatus('200 OK') + self.assertBody('visit #2') + + self.getPage('/varying_headers/') + self.assertStatus('200 OK') + self.assertBody('visit #1') + + def testExpiresTool(self): + # test setting an expires header + self.getPage('/expires/specific') + self.assertStatus('200 OK') + self.assertHeader('Expires') + + # test exceptions for bad time values + self.getPage('/expires/wrongtype') + self.assertStatus(500) + self.assertInBody('TypeError') + + # static content should not have "cache prevention" headers + self.getPage('/expires/index.html') + self.assertStatus('200 OK') + self.assertNoHeader('Pragma') + self.assertNoHeader('Cache-Control') + self.assertHeader('Expires') + + # dynamic content that sets indicators should not have + # "cache prevention" headers + self.getPage('/expires/cacheable') + self.assertStatus('200 OK') + self.assertNoHeader('Pragma') + self.assertNoHeader('Cache-Control') + self.assertHeader('Expires') + + self.getPage('/expires/dynamic') + self.assertBody('D-d-d-dynamic!') + # the Cache-Control header should be untouched + self.assertHeader('Cache-Control', 'private') + self.assertHeader('Expires') + + # configure the tool to ignore indicators and replace existing headers + self.getPage('/expires/force') + self.assertStatus('200 OK') + # This also gives us a chance to test 0 expiry with no other headers + self.assertHeader('Pragma', 'no-cache') + if cherrypy.server.protocol_version == 'HTTP/1.1': + self.assertHeader('Cache-Control', 'no-cache, must-revalidate') + self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') + + # static content should now have "cache prevention" headers + self.getPage('/expires/index.html') + self.assertStatus('200 OK') + self.assertHeader('Pragma', 'no-cache') + if cherrypy.server.protocol_version == 'HTTP/1.1': + self.assertHeader('Cache-Control', 'no-cache, must-revalidate') + self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') + + # the cacheable handler should now have "cache prevention" headers + self.getPage('/expires/cacheable') + self.assertStatus('200 OK') + self.assertHeader('Pragma', 'no-cache') + if cherrypy.server.protocol_version == 'HTTP/1.1': + self.assertHeader('Cache-Control', 'no-cache, must-revalidate') + self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') + + self.getPage('/expires/dynamic') + self.assertBody('D-d-d-dynamic!') + # dynamic sets Cache-Control to private but it should be + # overwritten here ... + self.assertHeader('Pragma', 'no-cache') + if cherrypy.server.protocol_version == 'HTTP/1.1': + self.assertHeader('Cache-Control', 'no-cache, must-revalidate') + self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') + + def _assert_resp_len_and_enc_for_gzip(self, uri): + """ + Test that after querying gzipped content it's remains valid in + cache and available non-gzipped as well. + """ + ACCEPT_GZIP_HEADERS = [('Accept-Encoding', 'gzip')] + content_len = None + + for _ in range(3): + self.getPage(uri, method='GET', headers=ACCEPT_GZIP_HEADERS) + + if content_len is not None: + # all requests should get the same length + self.assertHeader('Content-Length', content_len) + self.assertHeader('Content-Encoding', 'gzip') + + content_len = dict(self.headers)['Content-Length'] + + # check that we can still get non-gzipped version + self.getPage(uri, method='GET') + self.assertNoHeader('Content-Encoding') + # non-gzipped version should have a different content length + self.assertNoHeaderItemValue('Content-Length', content_len) + + def testGzipStaticCache(self): + """Test that cache and gzip tools play well together when both enabled. + + Ref GitHub issue #1190. + """ + GZIP_STATIC_CACHE_TMPL = '/gzip_static_cache/{}' + resource_files = ('index.html', 'dirback.jpg') + + for f in resource_files: + uri = GZIP_STATIC_CACHE_TMPL.format(f) + self._assert_resp_len_and_enc_for_gzip(uri) + + def testLastModified(self): + self.getPage('/a.gif') + self.assertStatus(200) + self.assertBody(gif_bytes) + lm1 = self.assertHeader('Last-Modified') + + # this request should get the cached copy. + self.getPage('/a.gif') + self.assertStatus(200) + self.assertBody(gif_bytes) + self.assertHeader('Age') + lm2 = self.assertHeader('Last-Modified') + self.assertEqual(lm1, lm2) + + # this request should match the cached copy, but raise 304. + self.getPage('/a.gif', [('If-Modified-Since', lm1)]) + self.assertStatus(304) + self.assertNoHeader('Last-Modified') + if not getattr(cherrypy.server, 'using_apache', False): + self.assertHeader('Age') + + @pytest.mark.xfail(reason='#1536') + def test_antistampede(self): + SECONDS = 4 + slow_url = '/long_process?seconds={SECONDS}'.format(**locals()) + # We MUST make an initial synchronous request in order to create the + # AntiStampedeCache object, and populate its selecting_headers, + # before the actual stampede. + self.getPage(slow_url) + self.assertBody('success!') + path = urllib.parse.quote(slow_url, safe='') + self.getPage('/clear_cache?path=' + path) + self.assertStatus(200) + + start = datetime.datetime.now() + + def run(): + self.getPage(slow_url) + # The response should be the same every time + self.assertBody('success!') + ts = [threading.Thread(target=run) for i in range(100)] + for t in ts: + t.start() + for t in ts: + t.join() + finish = datetime.datetime.now() + # Allow for overhead, two seconds for slow hosts + allowance = SECONDS + 2 + self.assertEqualDates(start, finish, seconds=allowance) + + def test_cache_control(self): + self.getPage('/control') + self.assertBody('visit #1') + self.getPage('/control') + self.assertBody('visit #1') + + self.getPage('/control', headers=[('Cache-Control', 'no-cache')]) + self.assertBody('visit #2') + self.getPage('/control') + self.assertBody('visit #2') + + self.getPage('/control', headers=[('Pragma', 'no-cache')]) + self.assertBody('visit #3') + self.getPage('/control') + self.assertBody('visit #3') + + time.sleep(1) + self.getPage('/control', headers=[('Cache-Control', 'max-age=0')]) + self.assertBody('visit #4') + self.getPage('/control') + self.assertBody('visit #4') diff --git a/resources/lib/cherrypy/test/test_compat.py b/resources/lib/cherrypy/test/test_compat.py new file mode 100644 index 0000000..44a9fa3 --- /dev/null +++ b/resources/lib/cherrypy/test/test_compat.py @@ -0,0 +1,34 @@ +"""Test Python 2/3 compatibility module.""" +from __future__ import unicode_literals + +import unittest + +import pytest +import six + +from cherrypy import _cpcompat as compat + + +class StringTester(unittest.TestCase): + """Tests for string conversion.""" + + @pytest.mark.skipif(six.PY3, reason='Only useful on Python 2') + def test_ntob_non_native(self): + """ntob should raise an Exception on unicode. + + (Python 2 only) + + See #1132 for discussion. + """ + self.assertRaises(TypeError, compat.ntob, 'fight') + + +class EscapeTester(unittest.TestCase): + """Class to test escape_html function from _cpcompat.""" + + def test_escape_quote(self): + """test_escape_quote - Verify the output for &<>"' chars.""" + self.assertEqual( + """xx&<>"aa'""", + compat.escape_html("""xx&<>"aa'"""), + ) diff --git a/resources/lib/cherrypy/test/test_config.py b/resources/lib/cherrypy/test/test_config.py new file mode 100644 index 0000000..be17df9 --- /dev/null +++ b/resources/lib/cherrypy/test/test_config.py @@ -0,0 +1,303 @@ +"""Tests for the CherryPy configuration system.""" + +import io +import os +import sys +import unittest + +import six + +import cherrypy + +from cherrypy.test import helper + + +localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +def StringIOFromNative(x): + return io.StringIO(six.text_type(x)) + + +def setup_server(): + + @cherrypy.config(foo='this', bar='that') + class Root: + + def __init__(self): + cherrypy.config.namespaces['db'] = self.db_namespace + + def db_namespace(self, k, v): + if k == 'scheme': + self.db = v + + @cherrypy.expose(alias=('global_', 'xyz')) + def index(self, key): + return cherrypy.request.config.get(key, 'None') + + @cherrypy.expose + def repr(self, key): + return repr(cherrypy.request.config.get(key, None)) + + @cherrypy.expose + def dbscheme(self): + return self.db + + @cherrypy.expose + @cherrypy.config(**{'request.body.attempt_charsets': ['utf-16']}) + def plain(self, x): + return x + + favicon_ico = cherrypy.tools.staticfile.handler( + filename=os.path.join(localDir, '../favicon.ico')) + + @cherrypy.config(foo='this2', baz='that2') + class Foo: + + @cherrypy.expose + def index(self, key): + return cherrypy.request.config.get(key, 'None') + nex = index + + @cherrypy.expose + @cherrypy.config(**{'response.headers.X-silly': 'sillyval'}) + def silly(self): + return 'Hello world' + + # Test the expose and config decorators + @cherrypy.config(foo='this3', **{'bax': 'this4'}) + @cherrypy.expose + def bar(self, key): + return repr(cherrypy.request.config.get(key, None)) + + class Another: + + @cherrypy.expose + def index(self, key): + return str(cherrypy.request.config.get(key, 'None')) + + def raw_namespace(key, value): + if key == 'input.map': + handler = cherrypy.request.handler + + def wrapper(): + params = cherrypy.request.params + for name, coercer in list(value.items()): + try: + params[name] = coercer(params[name]) + except KeyError: + pass + return handler() + cherrypy.request.handler = wrapper + elif key == 'output': + handler = cherrypy.request.handler + + def wrapper(): + # 'value' is a type (like int or str). + return value(handler()) + cherrypy.request.handler = wrapper + + @cherrypy.config(**{'raw.output': repr}) + class Raw: + + @cherrypy.expose + @cherrypy.config(**{'raw.input.map': {'num': int}}) + def incr(self, num): + return num + 1 + + if not six.PY3: + thing3 = "thing3: unicode('test', errors='ignore')" + else: + thing3 = '' + + ioconf = StringIOFromNative(""" +[/] +neg: -1234 +filename: os.path.join(sys.prefix, "hello.py") +thing1: cherrypy.lib.httputil.response_codes[404] +thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2 +%s +complex: 3+2j +mul: 6*3 +ones: "11" +twos: "22" +stradd: %%(ones)s + %%(twos)s + "33" + +[/favicon.ico] +tools.staticfile.filename = %r +""" % (thing3, os.path.join(localDir, 'static/dirback.jpg'))) + + root = Root() + root.foo = Foo() + root.raw = Raw() + app = cherrypy.tree.mount(root, config=ioconf) + app.request_class.namespaces['raw'] = raw_namespace + + cherrypy.tree.mount(Another(), '/another') + cherrypy.config.update({'luxuryyacht': 'throatwobblermangrove', + 'db.scheme': r'sqlite///memory', + }) + + +# Client-side code # + + +class ConfigTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def testConfig(self): + tests = [ + ('/', 'nex', 'None'), + ('/', 'foo', 'this'), + ('/', 'bar', 'that'), + ('/xyz', 'foo', 'this'), + ('/foo/', 'foo', 'this2'), + ('/foo/', 'bar', 'that'), + ('/foo/', 'bax', 'None'), + ('/foo/bar', 'baz', "'that2'"), + ('/foo/nex', 'baz', 'that2'), + # If 'foo' == 'this', then the mount point '/another' leaks into + # '/'. + ('/another/', 'foo', 'None'), + ] + for path, key, expected in tests: + self.getPage(path + '?key=' + key) + self.assertBody(expected) + + expectedconf = { + # From CP defaults + 'tools.log_headers.on': False, + 'tools.log_tracebacks.on': True, + 'request.show_tracebacks': True, + 'log.screen': False, + 'environment': 'test_suite', + 'engine.autoreload.on': False, + # From global config + 'luxuryyacht': 'throatwobblermangrove', + # From Root._cp_config + 'bar': 'that', + # From Foo._cp_config + 'baz': 'that2', + # From Foo.bar._cp_config + 'foo': 'this3', + 'bax': 'this4', + } + for key, expected in expectedconf.items(): + self.getPage('/foo/bar?key=' + key) + self.assertBody(repr(expected)) + + def testUnrepr(self): + self.getPage('/repr?key=neg') + self.assertBody('-1234') + + self.getPage('/repr?key=filename') + self.assertBody(repr(os.path.join(sys.prefix, 'hello.py'))) + + self.getPage('/repr?key=thing1') + self.assertBody(repr(cherrypy.lib.httputil.response_codes[404])) + + if not getattr(cherrypy.server, 'using_apache', False): + # The object ID's won't match up when using Apache, since the + # server and client are running in different processes. + self.getPage('/repr?key=thing2') + from cherrypy.tutorial import thing2 + self.assertBody(repr(thing2)) + + if not six.PY3: + self.getPage('/repr?key=thing3') + self.assertBody(repr(six.text_type('test'))) + + self.getPage('/repr?key=complex') + self.assertBody('(3+2j)') + + self.getPage('/repr?key=mul') + self.assertBody('18') + + self.getPage('/repr?key=stradd') + self.assertBody(repr('112233')) + + def testRespNamespaces(self): + self.getPage('/foo/silly') + self.assertHeader('X-silly', 'sillyval') + self.assertBody('Hello world') + + def testCustomNamespaces(self): + self.getPage('/raw/incr?num=12') + self.assertBody('13') + + self.getPage('/dbscheme') + self.assertBody(r'sqlite///memory') + + def testHandlerToolConfigOverride(self): + # Assert that config overrides tool constructor args. Above, we set + # the favicon in the page handler to be '../favicon.ico', + # but then overrode it in config to be './static/dirback.jpg'. + self.getPage('/favicon.ico') + self.assertBody(open(os.path.join(localDir, 'static/dirback.jpg'), + 'rb').read()) + + def test_request_body_namespace(self): + self.getPage('/plain', method='POST', headers=[ + ('Content-Type', 'application/x-www-form-urlencoded'), + ('Content-Length', '13')], + body=b'\xff\xfex\x00=\xff\xfea\x00b\x00c\x00') + self.assertBody('abc') + + +class VariableSubstitutionTests(unittest.TestCase): + setup_server = staticmethod(setup_server) + + def test_config(self): + from textwrap import dedent + + # variable substitution with [DEFAULT] + conf = dedent(""" + [DEFAULT] + dir = "/some/dir" + my.dir = %(dir)s + "/sub" + + [my] + my.dir = %(dir)s + "/my/dir" + my.dir2 = %(my.dir)s + '/dir2' + + """) + + fp = StringIOFromNative(conf) + + cherrypy.config.update(fp) + self.assertEqual(cherrypy.config['my']['my.dir'], '/some/dir/my/dir') + self.assertEqual(cherrypy.config['my'] + ['my.dir2'], '/some/dir/my/dir/dir2') + + +class CallablesInConfigTest(unittest.TestCase): + setup_server = staticmethod(setup_server) + + def test_call_with_literal_dict(self): + from textwrap import dedent + conf = dedent(""" + [my] + value = dict(**{'foo': 'bar'}) + """) + fp = StringIOFromNative(conf) + cherrypy.config.update(fp) + self.assertEqual(cherrypy.config['my']['value'], {'foo': 'bar'}) + + def test_call_with_kwargs(self): + from textwrap import dedent + conf = dedent(""" + [my] + value = dict(foo="buzz", **cherrypy._test_dict) + """) + test_dict = { + 'foo': 'bar', + 'bar': 'foo', + 'fizz': 'buzz' + } + cherrypy._test_dict = test_dict + fp = StringIOFromNative(conf) + cherrypy.config.update(fp) + test_dict['foo'] = 'buzz' + self.assertEqual(cherrypy.config['my']['value']['foo'], 'buzz') + self.assertEqual(cherrypy.config['my']['value'], test_dict) + del cherrypy._test_dict diff --git a/resources/lib/cherrypy/test/test_config_server.py b/resources/lib/cherrypy/test/test_config_server.py new file mode 100644 index 0000000..7b18353 --- /dev/null +++ b/resources/lib/cherrypy/test/test_config_server.py @@ -0,0 +1,126 @@ +"""Tests for the CherryPy configuration system.""" + +import os + +import cherrypy +from cherrypy.test import helper + + +localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) + + +# Client-side code # + + +class ServerConfigTests(helper.CPWebCase): + + @staticmethod + def setup_server(): + + class Root: + + @cherrypy.expose + def index(self): + return cherrypy.request.wsgi_environ['SERVER_PORT'] + + @cherrypy.expose + def upload(self, file): + return 'Size: %s' % len(file.file.read()) + + @cherrypy.expose + @cherrypy.config(**{'request.body.maxbytes': 100}) + def tinyupload(self): + return cherrypy.request.body.read() + + cherrypy.tree.mount(Root()) + + cherrypy.config.update({ + 'server.socket_host': '0.0.0.0', + 'server.socket_port': 9876, + 'server.max_request_body_size': 200, + 'server.max_request_header_size': 500, + 'server.socket_timeout': 0.5, + + # Test explicit server.instance + 'server.2.instance': 'cherrypy._cpwsgi_server.CPWSGIServer', + 'server.2.socket_port': 9877, + + # Test non-numeric + # Also test default server.instance = builtin server + 'server.yetanother.socket_port': 9878, + }) + + PORT = 9876 + + def testBasicConfig(self): + self.getPage('/') + self.assertBody(str(self.PORT)) + + def testAdditionalServers(self): + if self.scheme == 'https': + return self.skip('not available under ssl') + self.PORT = 9877 + self.getPage('/') + self.assertBody(str(self.PORT)) + self.PORT = 9878 + self.getPage('/') + self.assertBody(str(self.PORT)) + + def testMaxRequestSizePerHandler(self): + if getattr(cherrypy.server, 'using_apache', False): + return self.skip('skipped due to known Apache differences... ') + + self.getPage('/tinyupload', method='POST', + headers=[('Content-Type', 'text/plain'), + ('Content-Length', '100')], + body='x' * 100) + self.assertStatus(200) + self.assertBody('x' * 100) + + self.getPage('/tinyupload', method='POST', + headers=[('Content-Type', 'text/plain'), + ('Content-Length', '101')], + body='x' * 101) + self.assertStatus(413) + + def testMaxRequestSize(self): + if getattr(cherrypy.server, 'using_apache', False): + return self.skip('skipped due to known Apache differences... ') + + for size in (500, 5000, 50000): + self.getPage('/', headers=[('From', 'x' * 500)]) + self.assertStatus(413) + + # Test for https://github.com/cherrypy/cherrypy/issues/421 + # (Incorrect border condition in readline of SizeCheckWrapper). + # This hangs in rev 891 and earlier. + lines256 = 'x' * 248 + self.getPage('/', + headers=[('Host', '%s:%s' % (self.HOST, self.PORT)), + ('From', lines256)]) + + # Test upload + cd = ( + 'Content-Disposition: form-data; ' + 'name="file"; ' + 'filename="hello.txt"' + ) + body = '\r\n'.join([ + '--x', + cd, + 'Content-Type: text/plain', + '', + '%s', + '--x--']) + partlen = 200 - len(body) + b = body % ('x' * partlen) + h = [('Content-type', 'multipart/form-data; boundary=x'), + ('Content-Length', '%s' % len(b))] + self.getPage('/upload', h, 'POST', b) + self.assertBody('Size: %d' % partlen) + + b = body % ('x' * 200) + h = [('Content-type', 'multipart/form-data; boundary=x'), + ('Content-Length', '%s' % len(b))] + self.getPage('/upload', h, 'POST', b) + self.assertStatus(413) diff --git a/resources/lib/cherrypy/test/test_conn.py b/resources/lib/cherrypy/test/test_conn.py new file mode 100644 index 0000000..7d60c6f --- /dev/null +++ b/resources/lib/cherrypy/test/test_conn.py @@ -0,0 +1,873 @@ +"""Tests for TCP connection handling, including proper and timely close.""" + +import errno +import socket +import sys +import time + +import six +from six.moves import urllib +from six.moves.http_client import BadStatusLine, HTTPConnection, NotConnected + +import pytest + +from cheroot.test import webtest + +import cherrypy +from cherrypy._cpcompat import HTTPSConnection, ntob, tonative +from cherrypy.test import helper + + +timeout = 1 +pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN' + + +def setup_server(): + + def raise500(): + raise cherrypy.HTTPError(500) + + class Root: + + @cherrypy.expose + def index(self): + return pov + page1 = index + page2 = index + page3 = index + + @cherrypy.expose + def hello(self): + return 'Hello, world!' + + @cherrypy.expose + def timeout(self, t): + return str(cherrypy.server.httpserver.timeout) + + @cherrypy.expose + @cherrypy.config(**{'response.stream': True}) + def stream(self, set_cl=False): + if set_cl: + cherrypy.response.headers['Content-Length'] = 10 + + def content(): + for x in range(10): + yield str(x) + + return content() + + @cherrypy.expose + def error(self, code=500): + raise cherrypy.HTTPError(code) + + @cherrypy.expose + def upload(self): + if not cherrypy.request.method == 'POST': + raise AssertionError("'POST' != request.method %r" % + cherrypy.request.method) + return "thanks for '%s'" % cherrypy.request.body.read() + + @cherrypy.expose + def custom(self, response_code): + cherrypy.response.status = response_code + return 'Code = %s' % response_code + + @cherrypy.expose + @cherrypy.config(**{'hooks.on_start_resource': raise500}) + def err_before_read(self): + return 'ok' + + @cherrypy.expose + def one_megabyte_of_a(self): + return ['a' * 1024] * 1024 + + @cherrypy.expose + # Turn off the encoding tool so it doens't collapse + # our response body and reclaculate the Content-Length. + @cherrypy.config(**{'tools.encode.on': False}) + def custom_cl(self, body, cl): + cherrypy.response.headers['Content-Length'] = cl + if not isinstance(body, list): + body = [body] + newbody = [] + for chunk in body: + if isinstance(chunk, six.text_type): + chunk = chunk.encode('ISO-8859-1') + newbody.append(chunk) + return newbody + + cherrypy.tree.mount(Root()) + cherrypy.config.update({ + 'server.max_request_body_size': 1001, + 'server.socket_timeout': timeout, + }) + + +class ConnectionCloseTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_HTTP11(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + self.persistent = True + + # Make the first request and assert there's no "Connection: close". + self.getPage('/') + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertNoHeader('Connection') + + # Make another request on the same connection. + self.getPage('/page1') + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertNoHeader('Connection') + + # Test client-side close. + self.getPage('/page2', headers=[('Connection', 'close')]) + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertHeader('Connection', 'close') + + # Make another request on the same connection, which should error. + self.assertRaises(NotConnected, self.getPage, '/') + + def test_Streaming_no_len(self): + try: + self._streaming(set_cl=False) + finally: + try: + self.HTTP_CONN.close() + except (TypeError, AttributeError): + pass + + def test_Streaming_with_len(self): + try: + self._streaming(set_cl=True) + finally: + try: + self.HTTP_CONN.close() + except (TypeError, AttributeError): + pass + + def _streaming(self, set_cl): + if cherrypy.server.protocol_version == 'HTTP/1.1': + self.PROTOCOL = 'HTTP/1.1' + + self.persistent = True + + # Make the first request and assert there's no "Connection: close". + self.getPage('/') + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertNoHeader('Connection') + + # Make another, streamed request on the same connection. + if set_cl: + # When a Content-Length is provided, the content should stream + # without closing the connection. + self.getPage('/stream?set_cl=Yes') + self.assertHeader('Content-Length') + self.assertNoHeader('Connection', 'close') + self.assertNoHeader('Transfer-Encoding') + + self.assertStatus('200 OK') + self.assertBody('0123456789') + else: + # When no Content-Length response header is provided, + # streamed output will either close the connection, or use + # chunked encoding, to determine transfer-length. + self.getPage('/stream') + self.assertNoHeader('Content-Length') + self.assertStatus('200 OK') + self.assertBody('0123456789') + + chunked_response = False + for k, v in self.headers: + if k.lower() == 'transfer-encoding': + if str(v) == 'chunked': + chunked_response = True + + if chunked_response: + self.assertNoHeader('Connection', 'close') + else: + self.assertHeader('Connection', 'close') + + # Make another request on the same connection, which should + # error. + self.assertRaises(NotConnected, self.getPage, '/') + + # Try HEAD. See + # https://github.com/cherrypy/cherrypy/issues/864. + self.getPage('/stream', method='HEAD') + self.assertStatus('200 OK') + self.assertBody('') + self.assertNoHeader('Transfer-Encoding') + else: + self.PROTOCOL = 'HTTP/1.0' + + self.persistent = True + + # Make the first request and assert Keep-Alive. + self.getPage('/', headers=[('Connection', 'Keep-Alive')]) + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertHeader('Connection', 'Keep-Alive') + + # Make another, streamed request on the same connection. + if set_cl: + # When a Content-Length is provided, the content should + # stream without closing the connection. + self.getPage('/stream?set_cl=Yes', + headers=[('Connection', 'Keep-Alive')]) + self.assertHeader('Content-Length') + self.assertHeader('Connection', 'Keep-Alive') + self.assertNoHeader('Transfer-Encoding') + self.assertStatus('200 OK') + self.assertBody('0123456789') + else: + # When a Content-Length is not provided, + # the server should close the connection. + self.getPage('/stream', headers=[('Connection', 'Keep-Alive')]) + self.assertStatus('200 OK') + self.assertBody('0123456789') + + self.assertNoHeader('Content-Length') + self.assertNoHeader('Connection', 'Keep-Alive') + self.assertNoHeader('Transfer-Encoding') + + # Make another request on the same connection, which should + # error. + self.assertRaises(NotConnected, self.getPage, '/') + + def test_HTTP10_KeepAlive(self): + self.PROTOCOL = 'HTTP/1.0' + if self.scheme == 'https': + self.HTTP_CONN = HTTPSConnection + else: + self.HTTP_CONN = HTTPConnection + + # Test a normal HTTP/1.0 request. + self.getPage('/page2') + self.assertStatus('200 OK') + self.assertBody(pov) + # Apache, for example, may emit a Connection header even for HTTP/1.0 + # self.assertNoHeader("Connection") + + # Test a keep-alive HTTP/1.0 request. + self.persistent = True + + self.getPage('/page3', headers=[('Connection', 'Keep-Alive')]) + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertHeader('Connection', 'Keep-Alive') + + # Remove the keep-alive header again. + self.getPage('/page3') + self.assertStatus('200 OK') + self.assertBody(pov) + # Apache, for example, may emit a Connection header even for HTTP/1.0 + # self.assertNoHeader("Connection") + + +class PipelineTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_HTTP11_Timeout(self): + # If we timeout without sending any data, + # the server will close the conn with a 408. + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + # Connect but send nothing. + self.persistent = True + conn = self.HTTP_CONN + conn.auto_open = False + conn.connect() + + # Wait for our socket timeout + time.sleep(timeout * 2) + + # The request should have returned 408 already. + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.assertEqual(response.status, 408) + conn.close() + + # Connect but send half the headers only. + self.persistent = True + conn = self.HTTP_CONN + conn.auto_open = False + conn.connect() + conn.send(b'GET /hello HTTP/1.1') + conn.send(('Host: %s' % self.HOST).encode('ascii')) + + # Wait for our socket timeout + time.sleep(timeout * 2) + + # The conn should have already sent 408. + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.assertEqual(response.status, 408) + conn.close() + + def test_HTTP11_Timeout_after_request(self): + # If we timeout after at least one request has succeeded, + # the server will close the conn without 408. + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + # Make an initial request + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest('GET', '/timeout?t=%s' % timeout, skip_host=True) + conn.putheader('Host', self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(str(timeout)) + + # Make a second request on the same socket + conn._output(b'GET /hello HTTP/1.1') + conn._output(ntob('Host: %s' % self.HOST, 'ascii')) + conn._send_output() + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody('Hello, world!') + + # Wait for our socket timeout + time.sleep(timeout * 2) + + # Make another request on the same socket, which should error + conn._output(b'GET /hello HTTP/1.1') + conn._output(ntob('Host: %s' % self.HOST, 'ascii')) + conn._send_output() + response = conn.response_class(conn.sock, method='GET') + try: + response.begin() + except Exception: + if not isinstance(sys.exc_info()[1], + (socket.error, BadStatusLine)): + self.fail("Writing to timed out socket didn't fail" + ' as it should have: %s' % sys.exc_info()[1]) + else: + if response.status != 408: + self.fail("Writing to timed out socket didn't fail" + ' as it should have: %s' % + response.read()) + + conn.close() + + # Make another request on a new socket, which should work + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest('GET', '/', skip_host=True) + conn.putheader('Host', self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(pov) + + # Make another request on the same socket, + # but timeout on the headers + conn.send(b'GET /hello HTTP/1.1') + # Wait for our socket timeout + time.sleep(timeout * 2) + response = conn.response_class(conn.sock, method='GET') + try: + response.begin() + except Exception: + if not isinstance(sys.exc_info()[1], + (socket.error, BadStatusLine)): + self.fail("Writing to timed out socket didn't fail" + ' as it should have: %s' % sys.exc_info()[1]) + else: + self.fail("Writing to timed out socket didn't fail" + ' as it should have: %s' % + response.read()) + + conn.close() + + # Retry the request on a new connection, which should work + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest('GET', '/', skip_host=True) + conn.putheader('Host', self.HOST) + conn.endheaders() + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.assertEqual(response.status, 200) + self.body = response.read() + self.assertBody(pov) + conn.close() + + def test_HTTP11_pipelining(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + # Test pipelining. httplib doesn't support this directly. + self.persistent = True + conn = self.HTTP_CONN + + # Put request 1 + conn.putrequest('GET', '/hello', skip_host=True) + conn.putheader('Host', self.HOST) + conn.endheaders() + + for trial in range(5): + # Put next request + conn._output(b'GET /hello HTTP/1.1') + conn._output(ntob('Host: %s' % self.HOST, 'ascii')) + conn._send_output() + + # Retrieve previous response + response = conn.response_class(conn.sock, method='GET') + # there is a bug in python3 regarding the buffering of + # ``conn.sock``. Until that bug get's fixed we will + # monkey patch the ``response`` instance. + # https://bugs.python.org/issue23377 + if six.PY3: + response.fp = conn.sock.makefile('rb', 0) + response.begin() + body = response.read(13) + self.assertEqual(response.status, 200) + self.assertEqual(body, b'Hello, world!') + + # Retrieve final response + response = conn.response_class(conn.sock, method='GET') + response.begin() + body = response.read() + self.assertEqual(response.status, 200) + self.assertEqual(body, b'Hello, world!') + + conn.close() + + def test_100_Continue(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + self.persistent = True + conn = self.HTTP_CONN + + # Try a page without an Expect request header first. + # Note that httplib's response.begin automatically ignores + # 100 Continue responses, so we must manually check for it. + try: + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', self.HOST) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '4') + conn.endheaders() + conn.send(ntob("d'oh")) + response = conn.response_class(conn.sock, method='POST') + version, status, reason = response._read_status() + self.assertNotEqual(status, 100) + finally: + conn.close() + + # Now try a page with an Expect header... + try: + conn.connect() + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', self.HOST) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '17') + conn.putheader('Expect', '100-continue') + conn.endheaders() + response = conn.response_class(conn.sock, method='POST') + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + self.assertEqual(status, 100) + while True: + line = response.fp.readline().strip() + if line: + self.fail( + '100 Continue should not output any headers. Got %r' % + line) + else: + break + + # ...send the body + body = b'I am a small file' + conn.send(body) + + # ...get the final response + response.begin() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(200) + self.assertBody("thanks for '%s'" % body) + finally: + conn.close() + + +class ConnectionTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_readall_or_close(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + if self.scheme == 'https': + self.HTTP_CONN = HTTPSConnection + else: + self.HTTP_CONN = HTTPConnection + + # Test a max of 0 (the default) and then reset to what it was above. + old_max = cherrypy.server.max_request_body_size + for new_max in (0, old_max): + cherrypy.server.max_request_body_size = new_max + + self.persistent = True + conn = self.HTTP_CONN + + # Get a POST page with an error + conn.putrequest('POST', '/err_before_read', skip_host=True) + conn.putheader('Host', self.HOST) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '1000') + conn.putheader('Expect', '100-continue') + conn.endheaders() + response = conn.response_class(conn.sock, method='POST') + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + self.assertEqual(status, 100) + while True: + skip = response.fp.readline().strip() + if not skip: + break + + # ...send the body + conn.send(ntob('x' * 1000)) + + # ...get the final response + response.begin() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(500) + + # Now try a working page with an Expect header... + conn._output(b'POST /upload HTTP/1.1') + conn._output(ntob('Host: %s' % self.HOST, 'ascii')) + conn._output(b'Content-Type: text/plain') + conn._output(b'Content-Length: 17') + conn._output(b'Expect: 100-continue') + conn._send_output() + response = conn.response_class(conn.sock, method='POST') + + # ...assert and then skip the 100 response + version, status, reason = response._read_status() + self.assertEqual(status, 100) + while True: + skip = response.fp.readline().strip() + if not skip: + break + + # ...send the body + body = b'I am a small file' + conn.send(body) + + # ...get the final response + response.begin() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(200) + self.assertBody("thanks for '%s'" % body) + conn.close() + + def test_No_Message_Body(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + # Set our HTTP_CONN to an instance so it persists between requests. + self.persistent = True + + # Make the first request and assert there's no "Connection: close". + self.getPage('/') + self.assertStatus('200 OK') + self.assertBody(pov) + self.assertNoHeader('Connection') + + # Make a 204 request on the same connection. + self.getPage('/custom/204') + self.assertStatus(204) + self.assertNoHeader('Content-Length') + self.assertBody('') + self.assertNoHeader('Connection') + + # Make a 304 request on the same connection. + self.getPage('/custom/304') + self.assertStatus(304) + self.assertNoHeader('Content-Length') + self.assertBody('') + self.assertNoHeader('Connection') + + def test_Chunked_Encoding(self): + if cherrypy.server.protocol_version != 'HTTP/1.1': + return self.skip() + + if (hasattr(self, 'harness') and + 'modpython' in self.harness.__class__.__name__.lower()): + # mod_python forbids chunked encoding + return self.skip() + + self.PROTOCOL = 'HTTP/1.1' + + # Set our HTTP_CONN to an instance so it persists between requests. + self.persistent = True + conn = self.HTTP_CONN + + # Try a normal chunked request (with extensions) + body = ntob('8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n' + 'Content-Type: application/json\r\n' + '\r\n') + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', self.HOST) + conn.putheader('Transfer-Encoding', 'chunked') + conn.putheader('Trailer', 'Content-Type') + # Note that this is somewhat malformed: + # we shouldn't be sending Content-Length. + # RFC 2616 says the server should ignore it. + conn.putheader('Content-Length', '3') + conn.endheaders() + conn.send(body) + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus('200 OK') + self.assertBody("thanks for '%s'" % b'xx\r\nxxxxyyyyy') + + # Try a chunked request that exceeds server.max_request_body_size. + # Note that the delimiters and trailer are included. + body = ntob('3e3\r\n' + ('x' * 995) + '\r\n0\r\n\r\n') + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', self.HOST) + conn.putheader('Transfer-Encoding', 'chunked') + conn.putheader('Content-Type', 'text/plain') + # Chunked requests don't need a content-length + # # conn.putheader("Content-Length", len(body)) + conn.endheaders() + conn.send(body) + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(413) + conn.close() + + def test_Content_Length_in(self): + # Try a non-chunked request where Content-Length exceeds + # server.max_request_body_size. Assert error before body send. + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', self.HOST) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '9999') + conn.endheaders() + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(413) + self.assertBody('The entity sent with the request exceeds ' + 'the maximum allowed bytes.') + conn.close() + + def test_Content_Length_out_preheaders(self): + # Try a non-chunked response where Content-Length is less than + # the actual bytes in the response body. + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest('GET', '/custom_cl?body=I+have+too+many+bytes&cl=5', + skip_host=True) + conn.putheader('Host', self.HOST) + conn.endheaders() + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(500) + self.assertBody( + 'The requested resource returned more bytes than the ' + 'declared Content-Length.') + conn.close() + + def test_Content_Length_out_postheaders(self): + # Try a non-chunked response where Content-Length is less than + # the actual bytes in the response body. + self.persistent = True + conn = self.HTTP_CONN + conn.putrequest( + 'GET', '/custom_cl?body=I+too&body=+have+too+many&cl=5', + skip_host=True) + conn.putheader('Host', self.HOST) + conn.endheaders() + response = conn.getresponse() + self.status, self.headers, self.body = webtest.shb(response) + self.assertStatus(200) + self.assertBody('I too') + conn.close() + + def test_598(self): + tmpl = '{scheme}://{host}:{port}/one_megabyte_of_a/' + url = tmpl.format( + scheme=self.scheme, + host=self.HOST, + port=self.PORT, + ) + remote_data_conn = urllib.request.urlopen(url) + buf = remote_data_conn.read(512) + time.sleep(timeout * 0.6) + remaining = (1024 * 1024) - 512 + while remaining: + data = remote_data_conn.read(remaining) + if not data: + break + else: + buf += data + remaining -= len(data) + + self.assertEqual(len(buf), 1024 * 1024) + self.assertEqual(buf, ntob('a' * 1024 * 1024)) + self.assertEqual(remaining, 0) + remote_data_conn.close() + + +def setup_upload_server(): + + class Root: + @cherrypy.expose + def upload(self): + if not cherrypy.request.method == 'POST': + raise AssertionError("'POST' != request.method %r" % + cherrypy.request.method) + return "thanks for '%s'" % tonative(cherrypy.request.body.read()) + + cherrypy.tree.mount(Root()) + cherrypy.config.update({ + 'server.max_request_body_size': 1001, + 'server.socket_timeout': 10, + 'server.accepted_queue_size': 5, + 'server.accepted_queue_timeout': 0.1, + }) + + +reset_names = 'ECONNRESET', 'WSAECONNRESET' +socket_reset_errors = [ + getattr(errno, name) + for name in reset_names + if hasattr(errno, name) +] +'reset error numbers available on this platform' + +socket_reset_errors += [ + # Python 3.5 raises an http.client.RemoteDisconnected + # with this message + 'Remote end closed connection without response', +] + + +class LimitedRequestQueueTests(helper.CPWebCase): + setup_server = staticmethod(setup_upload_server) + + @pytest.mark.xfail(reason='#1535') + def test_queue_full(self): + conns = [] + overflow_conn = None + + try: + # Make 15 initial requests and leave them open, which should use + # all of wsgiserver's WorkerThreads and fill its Queue. + for i in range(15): + conn = self.HTTP_CONN(self.HOST, self.PORT) + conn.putrequest('POST', '/upload', skip_host=True) + conn.putheader('Host', self.HOST) + conn.putheader('Content-Type', 'text/plain') + conn.putheader('Content-Length', '4') + conn.endheaders() + conns.append(conn) + + # Now try a 16th conn, which should be closed by the + # server immediately. + overflow_conn = self.HTTP_CONN(self.HOST, self.PORT) + # Manually connect since httplib won't let us set a timeout + for res in socket.getaddrinfo(self.HOST, self.PORT, 0, + socket.SOCK_STREAM): + af, socktype, proto, canonname, sa = res + overflow_conn.sock = socket.socket(af, socktype, proto) + overflow_conn.sock.settimeout(5) + overflow_conn.sock.connect(sa) + break + + overflow_conn.putrequest('GET', '/', skip_host=True) + overflow_conn.putheader('Host', self.HOST) + overflow_conn.endheaders() + response = overflow_conn.response_class( + overflow_conn.sock, + method='GET', + ) + try: + response.begin() + except socket.error as exc: + if exc.args[0] in socket_reset_errors: + pass # Expected. + else: + tmpl = ( + 'Overflow conn did not get RST. ' + 'Got {exc.args!r} instead' + ) + raise AssertionError(tmpl.format(**locals())) + except BadStatusLine: + # This is a special case in OS X. Linux and Windows will + # RST correctly. + assert sys.platform == 'darwin' + else: + raise AssertionError('Overflow conn did not get RST ') + finally: + for conn in conns: + conn.send(b'done') + response = conn.response_class(conn.sock, method='POST') + response.begin() + self.body = response.read() + self.assertBody("thanks for 'done'") + self.assertEqual(response.status, 200) + conn.close() + if overflow_conn: + overflow_conn.close() + + +class BadRequestTests(helper.CPWebCase): + setup_server = staticmethod(setup_server) + + def test_No_CRLF(self): + self.persistent = True + + conn = self.HTTP_CONN + conn.send(b'GET /hello HTTP/1.1\n\n') + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.body = response.read() + self.assertBody('HTTP requires CRLF terminators') + conn.close() + + conn.connect() + conn.send(b'GET /hello HTTP/1.1\r\n\n') + response = conn.response_class(conn.sock, method='GET') + response.begin() + self.body = response.read() + self.assertBody('HTTP requires CRLF terminators') + conn.close() diff --git a/resources/lib/cherrypy/test/test_core.py b/resources/lib/cherrypy/test/test_core.py new file mode 100644 index 0000000..9834c1f --- /dev/null +++ b/resources/lib/cherrypy/test/test_core.py @@ -0,0 +1,823 @@ +# coding: utf-8 + +"""Basic tests for the CherryPy core: request handling.""" + +import os +import sys +import types + +import six + +import cherrypy +from cherrypy._cpcompat import ntou +from cherrypy import _cptools, tools +from cherrypy.lib import httputil, static + +from cherrypy.test._test_decorators import ExposeExamples +from cherrypy.test import helper + + +localDir = os.path.dirname(__file__) +favicon_path = os.path.join(os.getcwd(), localDir, '../favicon.ico') + +# Client-side code # + + +class CoreRequestHandlingTest(helper.CPWebCase): + + @staticmethod + def setup_server(): + class Root: + + @cherrypy.expose + def index(self): + return 'hello' + + favicon_ico = tools.staticfile.handler(filename=favicon_path) + + @cherrypy.expose + def defct(self, newct): + newct = 'text/%s' % newct + cherrypy.config.update({'tools.response_headers.on': True, + 'tools.response_headers.headers': + [('Content-Type', newct)]}) + + @cherrypy.expose + def baseurl(self, path_info, relative=None): + return cherrypy.url(path_info, relative=bool(relative)) + + root = Root() + root.expose_dec = ExposeExamples() + + class TestType(type): + + """Metaclass which automatically exposes all functions in each + subclass, and adds an instance of the subclass as an attribute + of root. + """ + def __init__(cls, name, bases, dct): + type.__init__(cls, name, bases, dct) + for value in six.itervalues(dct): + if isinstance(value, types.FunctionType): + value.exposed = True + setattr(root, name.lower(), cls()) + Test = TestType('Test', (object, ), {}) + + @cherrypy.config(**{'tools.trailing_slash.on': False}) + class URL(Test): + + def index(self, path_info, relative=None): + if relative != 'server': + relative = bool(relative) + return cherrypy.url(path_info, relative=relative) + + def leaf(self, path_info, relative=None): + if relative != 'server': + relative = bool(relative) + return cherrypy.url(path_info, relative=relative) + + def qs(self, qs): + return cherrypy.url(qs=qs) + + def log_status(): + Status.statuses.append(cherrypy.response.status) + cherrypy.tools.log_status = cherrypy.Tool( + 'on_end_resource', log_status) + + class Status(Test): + + def index(self): + return 'normal' + + def blank(self): + cherrypy.response.status = '' + + # According to RFC 2616, new status codes are OK as long as they + # are between 100 and 599. + + # Here is an illegal code... + def illegal(self): + cherrypy.response.status = 781 + return 'oops' + + # ...and here is an unknown but legal code. + def unknown(self): + cherrypy.response.status = '431 My custom error' + return 'funky' + + # Non-numeric code + def bad(self): + cherrypy.response.status = 'error' + return 'bad news' + + statuses = [] + + @cherrypy.config(**{'tools.log_status.on': True}) + def on_end_resource_stage(self): + return repr(self.statuses) + + class Redirect(Test): + + @cherrypy.config(**{ + 'tools.err_redirect.on': True, + 'tools.err_redirect.url': '/errpage', + 'tools.err_redirect.internal': False, + }) + class Error: + @cherrypy.expose + def index(self): + raise NameError('redirect_test') + + error = Error() + + def index(self): + return 'child' + + def custom(self, url, code): + raise cherrypy.HTTPRedirect(url, code) + + @cherrypy.config(**{'tools.trailing_slash.extra': True}) + def by_code(self, code): + raise cherrypy.HTTPRedirect('somewhere%20else', code) + + def nomodify(self): + raise cherrypy.HTTPRedirect('', 304) + + def proxy(self): + raise cherrypy.HTTPRedirect('proxy', 305) + + def stringify(self): + return str(cherrypy.HTTPRedirect('/')) + + def fragment(self, frag): + raise cherrypy.HTTPRedirect('/some/url#%s' % frag) + + def url_with_quote(self): + raise cherrypy.HTTPRedirect("/some\"url/that'we/want") + + def url_with_xss(self): + raise cherrypy.HTTPRedirect( + "/someurl/that'we/want") + + def url_with_unicode(self): + raise cherrypy.HTTPRedirect(ntou('тест', 'utf-8')) + + def login_redir(): + if not getattr(cherrypy.request, 'login', None): + raise cherrypy.InternalRedirect('/internalredirect/login') + tools.login_redir = _cptools.Tool('before_handler', login_redir) + + def redir_custom(): + raise cherrypy.InternalRedirect('/internalredirect/custom_err') + + class InternalRedirect(Test): + + def index(self): + raise cherrypy.InternalRedirect('/') + + @cherrypy.expose + @cherrypy.config(**{'hooks.before_error_response': redir_custom}) + def choke(self): + return 3 / 0 + + def relative(self, a, b): + raise cherrypy.InternalRedirect('cousin?t=6') + + def cousin(self, t): + assert cherrypy.request.prev.closed + return cherrypy.request.prev.query_string + + def petshop(self, user_id): + if user_id == 'parrot': + # Trade it for a slug when redirecting + raise cherrypy.InternalRedirect( + '/image/getImagesByUser?user_id=slug') + elif user_id == 'terrier': + # Trade it for a fish when redirecting + raise cherrypy.InternalRedirect( + '/image/getImagesByUser?user_id=fish') + else: + # This should pass the user_id through to getImagesByUser + raise cherrypy.InternalRedirect( + '/image/getImagesByUser?user_id=%s' % str(user_id)) + + # We support Python 2.3, but the @-deco syntax would look like + # this: + # @tools.login_redir() + def secure(self): + return 'Welcome!' + secure = tools.login_redir()(secure) + # Since calling the tool returns the same function you pass in, + # you could skip binding the return value, and just write: + # tools.login_redir()(secure) + + def login(self): + return 'Please log in' + + def custom_err(self): + return 'Something went horribly wrong.' + + @cherrypy.config(**{'hooks.before_request_body': redir_custom}) + def early_ir(self, arg): + return 'whatever' + + class Image(Test): + + def getImagesByUser(self, user_id): + return '0 images for %s' % user_id + + class Flatten(Test): + + def as_string(self): + return 'content' + + def as_list(self): + return ['con', 'tent'] + + def as_yield(self): + yield b'content' + + @cherrypy.config(**{'tools.flatten.on': True}) + def as_dblyield(self): + yield self.as_yield() + + def as_refyield(self): + for chunk in self.as_yield(): + yield chunk + + class Ranges(Test): + + def get_ranges(self, bytes): + return repr(httputil.get_ranges('bytes=%s' % bytes, 8)) + + def slice_file(self): + path = os.path.join(os.getcwd(), os.path.dirname(__file__)) + return static.serve_file( + os.path.join(path, 'static/index.html')) + + class Cookies(Test): + + def single(self, name): + cookie = cherrypy.request.cookie[name] + # Python2's SimpleCookie.__setitem__ won't take unicode keys. + cherrypy.response.cookie[str(name)] = cookie.value + + def multiple(self, names): + list(map(self.single, names)) + + def append_headers(header_list, debug=False): + if debug: + cherrypy.log( + 'Extending response headers with %s' % repr(header_list), + 'TOOLS.APPEND_HEADERS') + cherrypy.serving.response.header_list.extend(header_list) + cherrypy.tools.append_headers = cherrypy.Tool( + 'on_end_resource', append_headers) + + class MultiHeader(Test): + + def header_list(self): + pass + header_list = cherrypy.tools.append_headers(header_list=[ + (b'WWW-Authenticate', b'Negotiate'), + (b'WWW-Authenticate', b'Basic realm="foo"'), + ])(header_list) + + def commas(self): + cherrypy.response.headers[ + 'WWW-Authenticate'] = 'Negotiate,Basic realm="foo"' + + cherrypy.tree.mount(root) + + def testStatus(self): + self.getPage('/status/') + self.assertBody('normal') + self.assertStatus(200) + + self.getPage('/status/blank') + self.assertBody('') + self.assertStatus(200) + + self.getPage('/status/illegal') + self.assertStatus(500) + msg = 'Illegal response status from server (781 is out of range).' + self.assertErrorPage(500, msg) + + if not getattr(cherrypy.server, 'using_apache', False): + self.getPage('/status/unknown') + self.assertBody('funky') + self.assertStatus(431) + + self.getPage('/status/bad') + self.assertStatus(500) + msg = "Illegal response status from server ('error' is non-numeric)." + self.assertErrorPage(500, msg) + + def test_on_end_resource_status(self): + self.getPage('/status/on_end_resource_stage') + self.assertBody('[]') + self.getPage('/status/on_end_resource_stage') + self.assertBody(repr(['200 OK'])) + + def testSlashes(self): + # Test that requests for index methods without a trailing slash + # get redirected to the same URI path with a trailing slash. + # Make sure GET params are preserved. + self.getPage('/redirect?id=3') + self.assertStatus(301) + self.assertMatchesBody( + '' + '%s/redirect/[?]id=3' % (self.base(), self.base()) + ) + + if self.prefix(): + # Corner case: the "trailing slash" redirect could be tricky if + # we're using a virtual root and the URI is "/vroot" (no slash). + self.getPage('') + self.assertStatus(301) + self.assertMatchesBody("%s/" % + (self.base(), self.base())) + + # Test that requests for NON-index methods WITH a trailing slash + # get redirected to the same URI path WITHOUT a trailing slash. + # Make sure GET params are preserved. + self.getPage('/redirect/by_code/?code=307') + self.assertStatus(301) + self.assertMatchesBody( + "" + '%s/redirect/by_code[?]code=307' + % (self.base(), self.base()) + ) + + # If the trailing_slash tool is off, CP should just continue + # as if the slashes were correct. But it needs some help + # inside cherrypy.url to form correct output. + self.getPage('/url?path_info=page1') + self.assertBody('%s/url/page1' % self.base()) + self.getPage('/url/leaf/?path_info=page1') + self.assertBody('%s/url/page1' % self.base()) + + def testRedirect(self): + self.getPage('/redirect/') + self.assertBody('child') + self.assertStatus(200) + + self.getPage('/redirect/by_code?code=300') + self.assertMatchesBody( + r"\2somewhere%20else") + self.assertStatus(300) + + self.getPage('/redirect/by_code?code=301') + self.assertMatchesBody( + r"\2somewhere%20else") + self.assertStatus(301) + + self.getPage('/redirect/by_code?code=302') + self.assertMatchesBody( + r"\2somewhere%20else") + self.assertStatus(302) + + self.getPage('/redirect/by_code?code=303') + self.assertMatchesBody( + r"\2somewhere%20else") + self.assertStatus(303) + + self.getPage('/redirect/by_code?code=307') + self.assertMatchesBody( + r"\2somewhere%20else") + self.assertStatus(307) + + self.getPage('/redirect/nomodify') + self.assertBody('') + self.assertStatus(304) + + self.getPage('/redirect/proxy') + self.assertBody('') + self.assertStatus(305) + + # HTTPRedirect on error + self.getPage('/redirect/error/') + self.assertStatus(('302 Found', '303 See Other')) + self.assertInBody('/errpage') + + # Make sure str(HTTPRedirect()) works. + self.getPage('/redirect/stringify', protocol='HTTP/1.0') + self.assertStatus(200) + self.assertBody("(['%s/'], 302)" % self.base()) + if cherrypy.server.protocol_version == 'HTTP/1.1': + self.getPage('/redirect/stringify', protocol='HTTP/1.1') + self.assertStatus(200) + self.assertBody("(['%s/'], 303)" % self.base()) + + # check that #fragments are handled properly + # http://skrb.org/ietf/http_errata.html#location-fragments + frag = 'foo' + self.getPage('/redirect/fragment/%s' % frag) + self.assertMatchesBody( + r"\2\/some\/url\#%s" % ( + frag, frag)) + loc = self.assertHeader('Location') + assert loc.endswith('#%s' % frag) + self.assertStatus(('302 Found', '303 See Other')) + + # check injection protection + # See https://github.com/cherrypy/cherrypy/issues/1003 + self.getPage( + '/redirect/custom?' + 'code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval') + self.assertStatus(303) + loc = self.assertHeader('Location') + assert 'Set-Cookie' in loc + self.assertNoHeader('Set-Cookie') + + def assertValidXHTML(): + from xml.etree import ElementTree + try: + ElementTree.fromstring( + '%s' % self.body, + ) + except ElementTree.ParseError: + self._handlewebError( + 'automatically generated redirect did not ' + 'generate well-formed html', + ) + + # check redirects to URLs generated valid HTML - we check this + # by seeing if it appears as valid XHTML. + self.getPage('/redirect/by_code?code=303') + self.assertStatus(303) + assertValidXHTML() + + # do the same with a url containing quote characters. + self.getPage('/redirect/url_with_quote') + self.assertStatus(303) + assertValidXHTML() + + def test_redirect_with_xss(self): + """A redirect to a URL with HTML injected should result + in page contents escaped.""" + self.getPage('/redirect/url_with_xss') + self.assertStatus(303) + assert b' - - - -

Session Demo

-

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

-

Index | Expire | Regenerate

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

%(changemsg)s

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

Session Data%(sessiondata)s
Server Time%(servertime)s (Unix time: %(serverunixtime)s)
Browser Time 
Cherrypy Version:%(cpversion)s
Python Version:%(pyversion)s
- -""" # noqa E501 - - -class Root(object): - - def page(self): - changemsg = [] - if cherrypy.session.id != cherrypy.session.originalid: - if cherrypy.session.originalid is None: - changemsg.append( - 'Created new session because no session id was given.') - if cherrypy.session.missing: - changemsg.append( - 'Created new session due to missing ' - '(expired or malicious) session.') - if cherrypy.session.regenerated: - changemsg.append('Application generated a new session.') - - try: - expires = cherrypy.response.cookie['session_id']['expires'] - except KeyError: - expires = '' - - return page % { - 'sessionid': cherrypy.session.id, - 'changemsg': '
'.join(changemsg), - 'respcookie': cherrypy.response.cookie.output(), - 'reqcookie': cherrypy.request.cookie.output(), - 'sessiondata': list(cherrypy.session.items()), - 'servertime': ( - datetime.utcnow().strftime('%Y/%m/%d %H:%M') + ' UTC' - ), - 'serverunixtime': calendar.timegm(datetime.utcnow().timetuple()), - 'cpversion': cherrypy.__version__, - 'pyversion': sys.version, - 'expires': expires, - } - - @cherrypy.expose - def index(self): - # Must modify data or the session will not be saved. - cherrypy.session['color'] = 'green' - return self.page() - - @cherrypy.expose - def expire(self): - sessions.expire() - return self.page() - - @cherrypy.expose - def regen(self): - cherrypy.session.regenerate() - # Must modify data or the session will not be saved. - cherrypy.session['color'] = 'yellow' - return self.page() - - -if __name__ == '__main__': - cherrypy.config.update({ - # 'environment': 'production', - 'log.screen': True, - 'tools.sessions.on': True, - }) - cherrypy.quickstart(Root()) diff --git a/resources/lib/cherrypy/test/static/404.html b/resources/lib/cherrypy/test/static/404.html deleted file mode 100644 index 01b17b0..0000000 --- a/resources/lib/cherrypy/test/static/404.html +++ /dev/null @@ -1,5 +0,0 @@ - - -

I couldn't find that thing you were looking for!

- - diff --git a/resources/lib/cherrypy/test/static/dirback.jpg b/resources/lib/cherrypy/test/static/dirback.jpg deleted file mode 100644 index 80403dc227c19b9192158420f5288121e7f2669a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16585 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

9L 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 Sse&CiQ+RTS)_Y=q`u_k!0j`Vy diff --git a/resources/lib/cherrypy/test/static/index.html b/resources/lib/cherrypy/test/static/index.html deleted file mode 100644 index a5c1966..0000000 --- a/resources/lib/cherrypy/test/static/index.html +++ /dev/null @@ -1 +0,0 @@ -Hello, world diff --git a/resources/lib/cherrypy/test/style.css b/resources/lib/cherrypy/test/style.css deleted file mode 100644 index b266e93..0000000 --- a/resources/lib/cherrypy/test/style.css +++ /dev/null @@ -1 +0,0 @@ -Dummy stylesheet diff --git a/resources/lib/cherrypy/test/test.pem b/resources/lib/cherrypy/test/test.pem deleted file mode 100644 index 47a4704..0000000 --- a/resources/lib/cherrypy/test/test.pem +++ /dev/null @@ -1,38 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIICXAIBAAKBgQDBKo554mzIMY+AByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZ -R9L4WtImEew05FY3Izerfm3MN3+MC0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Kn -da+O6xldVSosu8Ev3z9VZ94iC/ZgKzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQAB -AoGAWOCF0ZrWxn3XMucWq2LNwPKqlvVGwbIwX3cDmX22zmnM4Fy6arXbYh4XlyCj -9+ofqRrxIFz5k/7tFriTmZ0xag5+Jdx+Kwg0/twiP7XCNKipFogwe1Hznw8OFAoT -enKBdj2+/n2o0Bvo/tDB59m9L/538d46JGQUmJlzMyqYikECQQDyoq+8CtMNvE18 -8VgHcR/KtApxWAjj4HpaHYL637ATjThetUZkW92mgDgowyplthusxdNqhHWyv7E8 -tWNdYErZAkEAy85ShTR0M5aWmrE7o0r0SpWInAkNBH9aXQRRARFYsdBtNfRu6I0i -0lvU9wiu3eF57FMEC86yViZ5UBnQfTu7vQJAVesj/Zt7pwaCDfdMa740OsxMUlyR -MVhhGx4OLpYdPJ8qUecxGQKq13XZ7R1HGyNEY4bd2X80Smq08UFuATfC6QJAH8UB -yBHtKz2GLIcELOg6PIYizW/7v3+6rlVF60yw7sb2vzpjL40QqIn4IKoR2DSVtOkb -8FtAIX3N21aq0VrGYQJBAIPiaEc2AZ8Bq2GC4F3wOz/BxJ/izvnkiotR12QK4fh5 -yjZMhTjWCas5zwHR5PDjlD88AWGDMsZ1PicD4348xJQ= ------END RSA PRIVATE KEY----- ------BEGIN CERTIFICATE----- -MIIDxTCCAy6gAwIBAgIJAI18BD7eQxlGMA0GCSqGSIb3DQEBBAUAMIGeMQswCQYD -VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU2FuIERpZWdv -MRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0MREwDwYDVQQLEwhkZXYtdGVzdDEW -MBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4GCSqGSIb3DQEJARYRcmVtaUBjaGVy -cnlweS5vcmcwHhcNMDYwOTA5MTkyMDIwWhcNMzQwMTI0MTkyMDIwWjCBnjELMAkG -A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVNhbiBEaWVn -bzEZMBcGA1UEChMQQ2hlcnJ5UHkgUHJvamVjdDERMA8GA1UECxMIZGV2LXRlc3Qx -FjAUBgNVBAMTDUNoZXJyeVB5IFRlYW0xIDAeBgkqhkiG9w0BCQEWEXJlbWlAY2hl -cnJ5cHkub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBKo554mzIMY+A -ByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZR9L4WtImEew05FY3Izerfm3MN3+M -C0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Knda+O6xldVSosu8Ev3z9VZ94iC/Zg -KzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQABo4IBBzCCAQMwHQYDVR0OBBYEFDIQ -2feb71tVZCWpU0qJ/Tw+wdtoMIHTBgNVHSMEgcswgciAFDIQ2feb71tVZCWpU0qJ -/Tw+wdtooYGkpIGhMIGeMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p -YTESMBAGA1UEBxMJU2FuIERpZWdvMRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0 -MREwDwYDVQQLEwhkZXYtdGVzdDEWMBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4G -CSqGSIb3DQEJARYRcmVtaUBjaGVycnlweS5vcmeCCQCNfAQ+3kMZRjAMBgNVHRME -BTADAQH/MA0GCSqGSIb3DQEBBAUAA4GBAL7AAQz7IePV48ZTAFHKr88ntPALsL5S -8vHCZPNMevNkLTj3DYUw2BcnENxMjm1kou2F2BkvheBPNZKIhc6z4hAml3ed1xa2 -D7w6e6OTcstdK/+KrPDDHeOP1dhMWNs2JE1bNlfF1LiXzYKSXpe88eCKjCXsCT/T -NluCaWQys3MS ------END CERTIFICATE----- diff --git a/resources/lib/cherrypy/test/test_auth_basic.py b/resources/lib/cherrypy/test/test_auth_basic.py deleted file mode 100644 index d7e69a9..0000000 --- a/resources/lib/cherrypy/test/test_auth_basic.py +++ /dev/null @@ -1,135 +0,0 @@ -# This file is part of CherryPy -# -*- coding: utf-8 -*- -# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 - -from hashlib import md5 - -import cherrypy -from cherrypy._cpcompat import ntob -from cherrypy.lib import auth_basic -from cherrypy.test import helper - - -class BasicAuthTest(helper.CPWebCase): - - @staticmethod - def setup_server(): - class Root: - - @cherrypy.expose - def index(self): - return 'This is public.' - - class BasicProtected: - - @cherrypy.expose - def index(self): - return "Hello %s, you've been authorized." % ( - cherrypy.request.login) - - class BasicProtected2: - - @cherrypy.expose - def index(self): - return "Hello %s, you've been authorized." % ( - cherrypy.request.login) - - class BasicProtected2_u: - - @cherrypy.expose - def index(self): - return "Hello %s, you've been authorized." % ( - cherrypy.request.login) - - userpassdict = {'xuser': 'xpassword'} - userhashdict = {'xuser': md5(b'xpassword').hexdigest()} - userhashdict_u = {'xюзер': md5(ntob('їжа', 'utf-8')).hexdigest()} - - def checkpasshash(realm, user, password): - p = userhashdict.get(user) - return p and p == md5(ntob(password)).hexdigest() or False - - def checkpasshash_u(realm, user, password): - p = userhashdict_u.get(user) - return p and p == md5(ntob(password, 'utf-8')).hexdigest() or False - - basic_checkpassword_dict = auth_basic.checkpassword_dict(userpassdict) - conf = { - '/basic': { - 'tools.auth_basic.on': True, - 'tools.auth_basic.realm': 'wonderland', - 'tools.auth_basic.checkpassword': basic_checkpassword_dict - }, - '/basic2': { - 'tools.auth_basic.on': True, - 'tools.auth_basic.realm': 'wonderland', - 'tools.auth_basic.checkpassword': checkpasshash, - 'tools.auth_basic.accept_charset': 'ISO-8859-1', - }, - '/basic2_u': { - 'tools.auth_basic.on': True, - 'tools.auth_basic.realm': 'wonderland', - 'tools.auth_basic.checkpassword': checkpasshash_u, - 'tools.auth_basic.accept_charset': 'UTF-8', - }, - } - - root = Root() - root.basic = BasicProtected() - root.basic2 = BasicProtected2() - root.basic2_u = BasicProtected2_u() - cherrypy.tree.mount(root, config=conf) - - def testPublic(self): - self.getPage('/') - self.assertStatus('200 OK') - self.assertHeader('Content-Type', 'text/html;charset=utf-8') - self.assertBody('This is public.') - - def testBasic(self): - self.getPage('/basic/') - self.assertStatus(401) - self.assertHeader( - 'WWW-Authenticate', - 'Basic realm="wonderland", charset="UTF-8"' - ) - - self.getPage('/basic/', - [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) - self.assertStatus(401) - - self.getPage('/basic/', - [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')]) - self.assertStatus('200 OK') - self.assertBody("Hello xuser, you've been authorized.") - - def testBasic2(self): - self.getPage('/basic2/') - self.assertStatus(401) - self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"') - - self.getPage('/basic2/', - [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')]) - self.assertStatus(401) - - self.getPage('/basic2/', - [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')]) - self.assertStatus('200 OK') - self.assertBody("Hello xuser, you've been authorized.") - - def testBasic2_u(self): - self.getPage('/basic2_u/') - self.assertStatus(401) - self.assertHeader( - 'WWW-Authenticate', - 'Basic realm="wonderland", charset="UTF-8"' - ) - - self.getPage('/basic2_u/', - [('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbRgw==')]) - self.assertStatus(401) - - self.getPage('/basic2_u/', - [('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbQsA==')]) - self.assertStatus('200 OK') - self.assertBody("Hello xюзер, you've been authorized.") diff --git a/resources/lib/cherrypy/test/test_auth_digest.py b/resources/lib/cherrypy/test/test_auth_digest.py deleted file mode 100644 index 745f89e..0000000 --- a/resources/lib/cherrypy/test/test_auth_digest.py +++ /dev/null @@ -1,131 +0,0 @@ -# This file is part of CherryPy -# -*- coding: utf-8 -*- -# vim:ts=4:sw=4:expandtab:fileencoding=utf-8 - - -import cherrypy -from cherrypy.lib import auth_digest -from cherrypy._cpcompat import ntob - -from cherrypy.test import helper - - -def _fetch_users(): - return {'test': 'test', '☃йюзер': 'їпароль'} - - -get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(_fetch_users()) - - -class DigestAuthTest(helper.CPWebCase): - - @staticmethod - def setup_server(): - class Root: - - @cherrypy.expose - def index(self): - return 'This is public.' - - class DigestProtected: - - @cherrypy.expose - def index(self, *args, **kwargs): - return "Hello %s, you've been authorized." % ( - cherrypy.request.login) - - conf = {'/digest': {'tools.auth_digest.on': True, - 'tools.auth_digest.realm': 'localhost', - 'tools.auth_digest.get_ha1': get_ha1, - 'tools.auth_digest.key': 'a565c27146791cfb', - 'tools.auth_digest.debug': True, - 'tools.auth_digest.accept_charset': 'UTF-8'}} - - root = Root() - root.digest = DigestProtected() - cherrypy.tree.mount(root, config=conf) - - def testPublic(self): - self.getPage('/') - assert self.status == '200 OK' - self.assertHeader('Content-Type', 'text/html;charset=utf-8') - assert self.body == b'This is public.' - - def _test_parametric_digest(self, username, realm): - test_uri = '/digest/?@/=%2F%40&%f0%9f%99%88=path' - - self.getPage(test_uri) - assert self.status_code == 401 - - msg = 'Digest authentification scheme was not found' - www_auth_digest = tuple(filter( - lambda kv: kv[0].lower() == 'www-authenticate' - and kv[1].startswith('Digest '), - self.headers, - )) - assert len(www_auth_digest) == 1, msg - - items = www_auth_digest[0][-1][7:].split(', ') - tokens = {} - for item in items: - key, value = item.split('=') - tokens[key.lower()] = value - - assert tokens['realm'] == '"localhost"' - assert tokens['algorithm'] == '"MD5"' - assert tokens['qop'] == '"auth"' - assert tokens['charset'] == '"UTF-8"' - - nonce = tokens['nonce'].strip('"') - - # Test user agent response with a wrong value for 'realm' - base_auth = ('Digest username="%s", ' - 'realm="%s", ' - 'nonce="%s", ' - 'uri="%s", ' - 'algorithm=MD5, ' - 'response="%s", ' - 'qop=auth, ' - 'nc=%s, ' - 'cnonce="1522e61005789929"') - - encoded_user = username - encoded_user = encoded_user.encode('utf-8') - encoded_user = encoded_user.decode('latin1') - auth_header = base_auth % ( - encoded_user, realm, nonce, test_uri, - '11111111111111111111111111111111', '00000001', - ) - auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET') - # calculate the response digest - ha1 = get_ha1(auth.realm, auth.username) - response = auth.request_digest(ha1) - auth_header = base_auth % ( - encoded_user, realm, nonce, test_uri, - response, '00000001', - ) - self.getPage(test_uri, [('Authorization', auth_header)]) - - def test_wrong_realm(self): - # send response with correct response digest, but wrong realm - self._test_parametric_digest(username='test', realm='wrong realm') - assert self.status_code == 401 - - def test_ascii_user(self): - self._test_parametric_digest(username='test', realm='localhost') - assert self.status == '200 OK' - assert self.body == b"Hello test, you've been authorized." - - def test_unicode_user(self): - self._test_parametric_digest(username='☃йюзер', realm='localhost') - assert self.status == '200 OK' - assert self.body == ntob( - "Hello ☃йюзер, you've been authorized.", 'utf-8', - ) - - def test_wrong_scheme(self): - basic_auth = { - 'Authorization': 'Basic foo:bar', - } - self.getPage('/digest/', headers=list(basic_auth.items())) - assert self.status_code == 401 diff --git a/resources/lib/cherrypy/test/test_bus.py b/resources/lib/cherrypy/test/test_bus.py deleted file mode 100644 index 83d89a6..0000000 --- a/resources/lib/cherrypy/test/test_bus.py +++ /dev/null @@ -1,313 +0,0 @@ -"""Publish-subscribe bus tests.""" -# pylint: disable=redefined-outer-name - -import threading -import time -import unittest.mock - -import pytest - -from cherrypy.process import wspbus - - -msg = 'Listener %d on channel %s: %s.' # pylint: disable=invalid-name - - -@pytest.fixture -def bus(): - """Return a wspbus instance.""" - return wspbus.Bus() - - -@pytest.fixture -def log_tracker(bus): - """Return an instance of bus log tracker.""" - class LogTracker: # pylint: disable=too-few-public-methods - """Bus log tracker.""" - - log_entries = [] - - def __init__(self, bus): - def logit(msg, level): # pylint: disable=unused-argument - self.log_entries.append(msg) - bus.subscribe('log', logit) - - return LogTracker(bus) - - -@pytest.fixture -def listener(): - """Return an instance of bus response tracker.""" - class Listner: # pylint: disable=too-few-public-methods - """Bus handler return value tracker.""" - - responses = [] - - def get_listener(self, channel, index): - """Return an argument tracking listener.""" - def listener(arg=None): - self.responses.append(msg % (index, channel, arg)) - return listener - - return Listner() - - -def test_builtin_channels(bus, listener): - """Test that built-in channels trigger corresponding listeners.""" - expected = [] - - for channel in bus.listeners: - for index, priority in enumerate([100, 50, 0, 51]): - bus.subscribe( - channel, - listener.get_listener(channel, index), - priority, - ) - - for channel in bus.listeners: - bus.publish(channel) - expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)]) - bus.publish(channel, arg=79347) - expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)]) - - assert listener.responses == expected - - -def test_custom_channels(bus, listener): - """Test that custom pub-sub channels work as built-in ones.""" - expected = [] - - custom_listeners = ('hugh', 'louis', 'dewey') - for channel in custom_listeners: - for index, priority in enumerate([None, 10, 60, 40]): - bus.subscribe( - channel, - listener.get_listener(channel, index), - priority, - ) - - for channel in custom_listeners: - bus.publish(channel, 'ah so') - expected.extend(msg % (i, channel, 'ah so') for i in (1, 3, 0, 2)) - bus.publish(channel) - expected.extend(msg % (i, channel, None) for i in (1, 3, 0, 2)) - - assert listener.responses == expected - - -def test_listener_errors(bus, listener): - """Test that unhandled exceptions raise channel failures.""" - expected = [] - channels = [c for c in bus.listeners if c != 'log'] - - for channel in channels: - bus.subscribe(channel, listener.get_listener(channel, 1)) - # This will break since the lambda takes no args. - bus.subscribe(channel, lambda: None, priority=20) - - for channel in channels: - with pytest.raises(wspbus.ChannelFailures): - bus.publish(channel, 123) - expected.append(msg % (1, channel, 123)) - - assert listener.responses == expected - - -def test_start(bus, listener, log_tracker): - """Test that bus start sequence calls all listeners.""" - num = 3 - for index in range(num): - bus.subscribe('start', listener.get_listener('start', index)) - - bus.start() - try: - # The start method MUST call all 'start' listeners. - assert ( - set(listener.responses) == - set(msg % (i, 'start', None) for i in range(num))) - # The start method MUST move the state to STARTED - # (or EXITING, if errors occur) - assert bus.state == bus.states.STARTED - # The start method MUST log its states. - assert log_tracker.log_entries == ['Bus STARTING', 'Bus STARTED'] - finally: - # Exit so the atexit handler doesn't complain. - bus.exit() - - -def test_stop(bus, listener, log_tracker): - """Test that bus stop sequence calls all listeners.""" - num = 3 - - for index in range(num): - bus.subscribe('stop', listener.get_listener('stop', index)) - - bus.stop() - - # The stop method MUST call all 'stop' listeners. - assert (set(listener.responses) == - set(msg % (i, 'stop', None) for i in range(num))) - - # The stop method MUST move the state to STOPPED - assert bus.state == bus.states.STOPPED - - # The stop method MUST log its states. - assert log_tracker.log_entries == ['Bus STOPPING', 'Bus STOPPED'] - - -def test_graceful(bus, listener, log_tracker): - """Test that bus graceful state triggers all listeners.""" - num = 3 - - for index in range(num): - bus.subscribe('graceful', listener.get_listener('graceful', index)) - - bus.graceful() - - # The graceful method MUST call all 'graceful' listeners. - assert ( - set(listener.responses) == - set(msg % (i, 'graceful', None) for i in range(num))) - - # The graceful method MUST log its states. - assert log_tracker.log_entries == ['Bus graceful'] - - -def test_exit(bus, listener, log_tracker): - """Test that bus exit sequence is correct.""" - num = 3 - - for index in range(num): - bus.subscribe('stop', listener.get_listener('stop', index)) - bus.subscribe('exit', listener.get_listener('exit', index)) - - bus.exit() - - # The exit method MUST call all 'stop' listeners, - # and then all 'exit' listeners. - assert (set(listener.responses) == - set([msg % (i, 'stop', None) for i in range(num)] + - [msg % (i, 'exit', None) for i in range(num)])) - - # The exit method MUST move the state to EXITING - assert bus.state == bus.states.EXITING - - # The exit method MUST log its states. - assert (log_tracker.log_entries == - ['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED']) - - -def test_wait(bus): - """Test that bus wait awaits for states.""" - def f(method): # pylint: disable=invalid-name - time.sleep(0.2) - getattr(bus, method)() - - flow = [ - ('start', [bus.states.STARTED]), - ('stop', [bus.states.STOPPED]), - ('start', [bus.states.STARTING, bus.states.STARTED]), - ('exit', [bus.states.EXITING]), - ] - - for method, states in flow: - threading.Thread(target=f, args=(method,)).start() - bus.wait(states) - - # The wait method MUST wait for the given state(s). - assert bus.state in states, 'State %r not in %r' % (bus.state, states) - - -def test_wait_publishes_periodically(bus): - """Test that wait publishes each tick.""" - callback = unittest.mock.MagicMock() - bus.subscribe('main', callback) - - def set_start(): - time.sleep(0.05) - bus.start() - threading.Thread(target=set_start).start() - bus.wait(bus.states.STARTED, interval=0.01, channel='main') - assert callback.call_count > 3 - - -def test_block(bus, log_tracker): - """Test that bus block waits for exiting.""" - def f(): # pylint: disable=invalid-name - time.sleep(0.2) - bus.exit() - - def g(): # pylint: disable=invalid-name - time.sleep(0.4) - - threading.Thread(target=f).start() - threading.Thread(target=g).start() - threads = [t for t in threading.enumerate() if not t.daemon] - assert len(threads) == 3 - - bus.block() - - # The block method MUST wait for the EXITING state. - assert bus.state == bus.states.EXITING - - # The block method MUST wait for ALL non-main, non-daemon threads to - # finish. - threads = [t for t in threading.enumerate() if not t.daemon] - assert len(threads) == 1 - - # The last message will mention an indeterminable thread name; ignore - # it - assert (log_tracker.log_entries[:-1] == - ['Bus STOPPING', 'Bus STOPPED', - 'Bus EXITING', 'Bus EXITED', - 'Waiting for child threads to terminate...']) - - -def test_start_with_callback(bus): - """Test that callback fires on bus start.""" - try: - events = [] - - def f(*args, **kwargs): # pylint: disable=invalid-name - events.append(('f', args, kwargs)) - - def g(): # pylint: disable=invalid-name - events.append('g') - bus.subscribe('start', g) - bus.start_with_callback(f, (1, 3, 5), {'foo': 'bar'}) - - # Give wait() time to run f() - time.sleep(0.2) - - # The callback method MUST wait for the STARTED state. - assert bus.state == bus.states.STARTED - - # The callback method MUST run after all start methods. - assert events == ['g', ('f', (1, 3, 5), {'foo': 'bar'})] - finally: - bus.exit() - - -def test_log(bus, log_tracker): - """Test that bus messages and errors are logged.""" - assert log_tracker.log_entries == [] - - # Try a normal message. - expected = [] - for msg_ in ["O mah darlin'"] * 3 + ['Clementiiiiiiiine']: - bus.log(msg_) - expected.append(msg_) - assert log_tracker.log_entries == expected - - # Try an error message - try: - foo - except NameError: - bus.log('You are lost and gone forever', traceback=True) - lastmsg = log_tracker.log_entries[-1] - assert 'Traceback' in lastmsg and 'NameError' in lastmsg, ( - 'Last log message %r did not contain ' - 'the expected traceback.' % lastmsg - ) - else: - pytest.fail('NameError was not raised as expected.') diff --git a/resources/lib/cherrypy/test/test_caching.py b/resources/lib/cherrypy/test/test_caching.py deleted file mode 100644 index c0b8979..0000000 --- a/resources/lib/cherrypy/test/test_caching.py +++ /dev/null @@ -1,390 +0,0 @@ -import datetime -from itertools import count -import os -import threading -import time -import urllib.parse - -import pytest - -import cherrypy -from cherrypy.lib import httputil - -from cherrypy.test import helper - - -curdir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - -gif_bytes = ( - b'GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00' - b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00' - b'\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;' -) - - -class CacheTest(helper.CPWebCase): - - @staticmethod - def setup_server(): - - @cherrypy.config(**{'tools.caching.on': True}) - class Root: - - def __init__(self): - self.counter = 0 - self.control_counter = 0 - self.longlock = threading.Lock() - - @cherrypy.expose - def index(self): - self.counter += 1 - msg = 'visit #%s' % self.counter - return msg - - @cherrypy.expose - def control(self): - self.control_counter += 1 - return 'visit #%s' % self.control_counter - - @cherrypy.expose - def a_gif(self): - cherrypy.response.headers[ - 'Last-Modified'] = httputil.HTTPDate() - return gif_bytes - - @cherrypy.expose - def long_process(self, seconds='1'): - try: - self.longlock.acquire() - time.sleep(float(seconds)) - finally: - self.longlock.release() - return 'success!' - - @cherrypy.expose - def clear_cache(self, path): - cherrypy._cache.store[cherrypy.request.base + path].clear() - - @cherrypy.config(**{ - 'tools.caching.on': True, - 'tools.response_headers.on': True, - 'tools.response_headers.headers': [ - ('Vary', 'Our-Varying-Header') - ], - }) - class VaryHeaderCachingServer(object): - - def __init__(self): - self.counter = count(1) - - @cherrypy.expose - def index(self): - return 'visit #%s' % next(self.counter) - - @cherrypy.config(**{ - 'tools.expires.on': True, - 'tools.expires.secs': 60, - 'tools.staticdir.on': True, - 'tools.staticdir.dir': 'static', - 'tools.staticdir.root': curdir, - }) - class UnCached(object): - - @cherrypy.expose - @cherrypy.config(**{'tools.expires.secs': 0}) - def force(self): - cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' - self._cp_config['tools.expires.force'] = True - self._cp_config['tools.expires.secs'] = 0 - return 'being forceful' - - @cherrypy.expose - def dynamic(self): - cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' - cherrypy.response.headers['Cache-Control'] = 'private' - return 'D-d-d-dynamic!' - - @cherrypy.expose - def cacheable(self): - cherrypy.response.headers['Etag'] = 'bibbitybobbityboo' - return "Hi, I'm cacheable." - - @cherrypy.expose - @cherrypy.config(**{'tools.expires.secs': 86400}) - def specific(self): - cherrypy.response.headers[ - 'Etag'] = 'need_this_to_make_me_cacheable' - return 'I am being specific' - - class Foo(object): - pass - - @cherrypy.expose - @cherrypy.config(**{'tools.expires.secs': Foo()}) - def wrongtype(self): - cherrypy.response.headers[ - 'Etag'] = 'need_this_to_make_me_cacheable' - return 'Woops' - - @cherrypy.config(**{ - 'tools.gzip.mime_types': ['text/*', 'image/*'], - 'tools.caching.on': True, - 'tools.staticdir.on': True, - 'tools.staticdir.dir': 'static', - 'tools.staticdir.root': curdir - }) - class GzipStaticCache(object): - pass - - cherrypy.tree.mount(Root()) - cherrypy.tree.mount(UnCached(), '/expires') - cherrypy.tree.mount(VaryHeaderCachingServer(), '/varying_headers') - cherrypy.tree.mount(GzipStaticCache(), '/gzip_static_cache') - cherrypy.config.update({'tools.gzip.on': True}) - - def testCaching(self): - elapsed = 0.0 - for trial in range(10): - self.getPage('/') - # The response should be the same every time, - # except for the Age response header. - self.assertBody('visit #1') - if trial != 0: - age = int(self.assertHeader('Age')) - assert age >= elapsed - elapsed = age - - # POST, PUT, DELETE should not be cached. - self.getPage('/', method='POST') - self.assertBody('visit #2') - # Because gzip is turned on, the Vary header should always Vary for - # content-encoding - self.assertHeader('Vary', 'Accept-Encoding') - # The previous request should have invalidated the cache, - # so this request will recalc the response. - self.getPage('/', method='GET') - self.assertBody('visit #3') - # ...but this request should get the cached copy. - self.getPage('/', method='GET') - self.assertBody('visit #3') - self.getPage('/', method='DELETE') - self.assertBody('visit #4') - - # The previous request should have invalidated the cache, - # so this request will recalc the response. - self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')]) - self.assertHeader('Content-Encoding', 'gzip') - self.assertHeader('Vary') - self.assertEqual( - cherrypy.lib.encoding.decompress(self.body), b'visit #5') - - # Now check that a second request gets the gzip header and gzipped body - # This also tests a bug in 3.0 to 3.0.2 whereby the cached, gzipped - # response body was being gzipped a second time. - self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')]) - self.assertHeader('Content-Encoding', 'gzip') - self.assertEqual( - cherrypy.lib.encoding.decompress(self.body), b'visit #5') - - # Now check that a third request that doesn't accept gzip - # skips the cache (because the 'Vary' header denies it). - self.getPage('/', method='GET') - self.assertNoHeader('Content-Encoding') - self.assertBody('visit #6') - - def testVaryHeader(self): - self.getPage('/varying_headers/') - self.assertStatus('200 OK') - self.assertHeaderItemValue('Vary', 'Our-Varying-Header') - self.assertBody('visit #1') - - # Now check that different 'Vary'-fields don't evict each other. - # This test creates 2 requests with different 'Our-Varying-Header' - # and then tests if the first one still exists. - self.getPage('/varying_headers/', - headers=[('Our-Varying-Header', 'request 2')]) - self.assertStatus('200 OK') - self.assertBody('visit #2') - - self.getPage('/varying_headers/', - headers=[('Our-Varying-Header', 'request 2')]) - self.assertStatus('200 OK') - self.assertBody('visit #2') - - self.getPage('/varying_headers/') - self.assertStatus('200 OK') - self.assertBody('visit #1') - - def testExpiresTool(self): - # test setting an expires header - self.getPage('/expires/specific') - self.assertStatus('200 OK') - self.assertHeader('Expires') - - # test exceptions for bad time values - self.getPage('/expires/wrongtype') - self.assertStatus(500) - self.assertInBody('TypeError') - - # static content should not have "cache prevention" headers - self.getPage('/expires/index.html') - self.assertStatus('200 OK') - self.assertNoHeader('Pragma') - self.assertNoHeader('Cache-Control') - self.assertHeader('Expires') - - # dynamic content that sets indicators should not have - # "cache prevention" headers - self.getPage('/expires/cacheable') - self.assertStatus('200 OK') - self.assertNoHeader('Pragma') - self.assertNoHeader('Cache-Control') - self.assertHeader('Expires') - - self.getPage('/expires/dynamic') - self.assertBody('D-d-d-dynamic!') - # the Cache-Control header should be untouched - self.assertHeader('Cache-Control', 'private') - self.assertHeader('Expires') - - # configure the tool to ignore indicators and replace existing headers - self.getPage('/expires/force') - self.assertStatus('200 OK') - # This also gives us a chance to test 0 expiry with no other headers - self.assertHeader('Pragma', 'no-cache') - if cherrypy.server.protocol_version == 'HTTP/1.1': - self.assertHeader('Cache-Control', 'no-cache, must-revalidate') - self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') - - # static content should now have "cache prevention" headers - self.getPage('/expires/index.html') - self.assertStatus('200 OK') - self.assertHeader('Pragma', 'no-cache') - if cherrypy.server.protocol_version == 'HTTP/1.1': - self.assertHeader('Cache-Control', 'no-cache, must-revalidate') - self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') - - # the cacheable handler should now have "cache prevention" headers - self.getPage('/expires/cacheable') - self.assertStatus('200 OK') - self.assertHeader('Pragma', 'no-cache') - if cherrypy.server.protocol_version == 'HTTP/1.1': - self.assertHeader('Cache-Control', 'no-cache, must-revalidate') - self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') - - self.getPage('/expires/dynamic') - self.assertBody('D-d-d-dynamic!') - # dynamic sets Cache-Control to private but it should be - # overwritten here ... - self.assertHeader('Pragma', 'no-cache') - if cherrypy.server.protocol_version == 'HTTP/1.1': - self.assertHeader('Cache-Control', 'no-cache, must-revalidate') - self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT') - - def _assert_resp_len_and_enc_for_gzip(self, uri): - """ - Test that after querying gzipped content it's remains valid in - cache and available non-gzipped as well. - """ - ACCEPT_GZIP_HEADERS = [('Accept-Encoding', 'gzip')] - content_len = None - - for _ in range(3): - self.getPage(uri, method='GET', headers=ACCEPT_GZIP_HEADERS) - - if content_len is not None: - # all requests should get the same length - self.assertHeader('Content-Length', content_len) - self.assertHeader('Content-Encoding', 'gzip') - - content_len = dict(self.headers)['Content-Length'] - - # check that we can still get non-gzipped version - self.getPage(uri, method='GET') - self.assertNoHeader('Content-Encoding') - # non-gzipped version should have a different content length - self.assertNoHeaderItemValue('Content-Length', content_len) - - def testGzipStaticCache(self): - """Test that cache and gzip tools play well together when both enabled. - - Ref GitHub issue #1190. - """ - GZIP_STATIC_CACHE_TMPL = '/gzip_static_cache/{}' - resource_files = ('index.html', 'dirback.jpg') - - for f in resource_files: - uri = GZIP_STATIC_CACHE_TMPL.format(f) - self._assert_resp_len_and_enc_for_gzip(uri) - - def testLastModified(self): - self.getPage('/a.gif') - self.assertStatus(200) - self.assertBody(gif_bytes) - lm1 = self.assertHeader('Last-Modified') - - # this request should get the cached copy. - self.getPage('/a.gif') - self.assertStatus(200) - self.assertBody(gif_bytes) - self.assertHeader('Age') - lm2 = self.assertHeader('Last-Modified') - self.assertEqual(lm1, lm2) - - # this request should match the cached copy, but raise 304. - self.getPage('/a.gif', [('If-Modified-Since', lm1)]) - self.assertStatus(304) - self.assertNoHeader('Last-Modified') - if not getattr(cherrypy.server, 'using_apache', False): - self.assertHeader('Age') - - @pytest.mark.xfail(reason='#1536') - def test_antistampede(self): - SECONDS = 4 - slow_url = '/long_process?seconds={SECONDS}'.format(**locals()) - # We MUST make an initial synchronous request in order to create the - # AntiStampedeCache object, and populate its selecting_headers, - # before the actual stampede. - self.getPage(slow_url) - self.assertBody('success!') - path = urllib.parse.quote(slow_url, safe='') - self.getPage('/clear_cache?path=' + path) - self.assertStatus(200) - - start = datetime.datetime.now() - - def run(): - self.getPage(slow_url) - # The response should be the same every time - self.assertBody('success!') - ts = [threading.Thread(target=run) for i in range(100)] - for t in ts: - t.start() - for t in ts: - t.join() - finish = datetime.datetime.now() - # Allow for overhead, two seconds for slow hosts - allowance = SECONDS + 2 - self.assertEqualDates(start, finish, seconds=allowance) - - def test_cache_control(self): - self.getPage('/control') - self.assertBody('visit #1') - self.getPage('/control') - self.assertBody('visit #1') - - self.getPage('/control', headers=[('Cache-Control', 'no-cache')]) - self.assertBody('visit #2') - self.getPage('/control') - self.assertBody('visit #2') - - self.getPage('/control', headers=[('Pragma', 'no-cache')]) - self.assertBody('visit #3') - self.getPage('/control') - self.assertBody('visit #3') - - time.sleep(1) - self.getPage('/control', headers=[('Cache-Control', 'max-age=0')]) - self.assertBody('visit #4') - self.getPage('/control') - self.assertBody('visit #4') diff --git a/resources/lib/cherrypy/test/test_compat.py b/resources/lib/cherrypy/test/test_compat.py deleted file mode 100644 index 44a9fa3..0000000 --- a/resources/lib/cherrypy/test/test_compat.py +++ /dev/null @@ -1,34 +0,0 @@ -"""Test Python 2/3 compatibility module.""" -from __future__ import unicode_literals - -import unittest - -import pytest -import six - -from cherrypy import _cpcompat as compat - - -class StringTester(unittest.TestCase): - """Tests for string conversion.""" - - @pytest.mark.skipif(six.PY3, reason='Only useful on Python 2') - def test_ntob_non_native(self): - """ntob should raise an Exception on unicode. - - (Python 2 only) - - See #1132 for discussion. - """ - self.assertRaises(TypeError, compat.ntob, 'fight') - - -class EscapeTester(unittest.TestCase): - """Class to test escape_html function from _cpcompat.""" - - def test_escape_quote(self): - """test_escape_quote - Verify the output for &<>"' chars.""" - self.assertEqual( - """xx&<>"aa'""", - compat.escape_html("""xx&<>"aa'"""), - ) diff --git a/resources/lib/cherrypy/test/test_config.py b/resources/lib/cherrypy/test/test_config.py deleted file mode 100644 index 5e880d8..0000000 --- a/resources/lib/cherrypy/test/test_config.py +++ /dev/null @@ -1,291 +0,0 @@ -"""Tests for the CherryPy configuration system.""" - -import io -import os -import sys -import unittest - -import cherrypy - -from cherrypy.test import helper - - -localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - - -def StringIOFromNative(x): - return io.StringIO(str(x)) - - -def setup_server(): - - @cherrypy.config(foo='this', bar='that') - class Root: - - def __init__(self): - cherrypy.config.namespaces['db'] = self.db_namespace - - def db_namespace(self, k, v): - if k == 'scheme': - self.db = v - - @cherrypy.expose(alias=('global_', 'xyz')) - def index(self, key): - return cherrypy.request.config.get(key, 'None') - - @cherrypy.expose - def repr(self, key): - return repr(cherrypy.request.config.get(key, None)) - - @cherrypy.expose - def dbscheme(self): - return self.db - - @cherrypy.expose - @cherrypy.config(**{'request.body.attempt_charsets': ['utf-16']}) - def plain(self, x): - return x - - favicon_ico = cherrypy.tools.staticfile.handler( - filename=os.path.join(localDir, '../favicon.ico')) - - @cherrypy.config(foo='this2', baz='that2') - class Foo: - - @cherrypy.expose - def index(self, key): - return cherrypy.request.config.get(key, 'None') - nex = index - - @cherrypy.expose - @cherrypy.config(**{'response.headers.X-silly': 'sillyval'}) - def silly(self): - return 'Hello world' - - # Test the expose and config decorators - @cherrypy.config(foo='this3', **{'bax': 'this4'}) - @cherrypy.expose - def bar(self, key): - return repr(cherrypy.request.config.get(key, None)) - - class Another: - - @cherrypy.expose - def index(self, key): - return str(cherrypy.request.config.get(key, 'None')) - - def raw_namespace(key, value): - if key == 'input.map': - handler = cherrypy.request.handler - - def wrapper(): - params = cherrypy.request.params - for name, coercer in value.copy().items(): - try: - params[name] = coercer(params[name]) - except KeyError: - pass - return handler() - cherrypy.request.handler = wrapper - elif key == 'output': - handler = cherrypy.request.handler - - def wrapper(): - # 'value' is a type (like int or str). - return value(handler()) - cherrypy.request.handler = wrapper - - @cherrypy.config(**{'raw.output': repr}) - class Raw: - - @cherrypy.expose - @cherrypy.config(**{'raw.input.map': {'num': int}}) - def incr(self, num): - return num + 1 - - ioconf = StringIOFromNative(""" -[/] -neg: -1234 -filename: os.path.join(sys.prefix, "hello.py") -thing1: cherrypy.lib.httputil.response_codes[404] -thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2 -complex: 3+2j -mul: 6*3 -ones: "11" -twos: "22" -stradd: %%(ones)s + %%(twos)s + "33" - -[/favicon.ico] -tools.staticfile.filename = %r -""" % os.path.join(localDir, 'static/dirback.jpg')) - - root = Root() - root.foo = Foo() - root.raw = Raw() - app = cherrypy.tree.mount(root, config=ioconf) - app.request_class.namespaces['raw'] = raw_namespace - - cherrypy.tree.mount(Another(), '/another') - cherrypy.config.update({'luxuryyacht': 'throatwobblermangrove', - 'db.scheme': r'sqlite///memory', - }) - - -# Client-side code # - - -class ConfigTests(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def testConfig(self): - tests = [ - ('/', 'nex', 'None'), - ('/', 'foo', 'this'), - ('/', 'bar', 'that'), - ('/xyz', 'foo', 'this'), - ('/foo/', 'foo', 'this2'), - ('/foo/', 'bar', 'that'), - ('/foo/', 'bax', 'None'), - ('/foo/bar', 'baz', "'that2'"), - ('/foo/nex', 'baz', 'that2'), - # If 'foo' == 'this', then the mount point '/another' leaks into - # '/'. - ('/another/', 'foo', 'None'), - ] - for path, key, expected in tests: - self.getPage(path + '?key=' + key) - self.assertBody(expected) - - expectedconf = { - # From CP defaults - 'tools.log_headers.on': False, - 'tools.log_tracebacks.on': True, - 'request.show_tracebacks': True, - 'log.screen': False, - 'environment': 'test_suite', - 'engine.autoreload.on': False, - # From global config - 'luxuryyacht': 'throatwobblermangrove', - # From Root._cp_config - 'bar': 'that', - # From Foo._cp_config - 'baz': 'that2', - # From Foo.bar._cp_config - 'foo': 'this3', - 'bax': 'this4', - } - for key, expected in expectedconf.items(): - self.getPage('/foo/bar?key=' + key) - self.assertBody(repr(expected)) - - def testUnrepr(self): - self.getPage('/repr?key=neg') - self.assertBody('-1234') - - self.getPage('/repr?key=filename') - self.assertBody(repr(os.path.join(sys.prefix, 'hello.py'))) - - self.getPage('/repr?key=thing1') - self.assertBody(repr(cherrypy.lib.httputil.response_codes[404])) - - if not getattr(cherrypy.server, 'using_apache', False): - # The object ID's won't match up when using Apache, since the - # server and client are running in different processes. - self.getPage('/repr?key=thing2') - from cherrypy.tutorial import thing2 - self.assertBody(repr(thing2)) - - self.getPage('/repr?key=complex') - self.assertBody('(3+2j)') - - self.getPage('/repr?key=mul') - self.assertBody('18') - - self.getPage('/repr?key=stradd') - self.assertBody(repr('112233')) - - def testRespNamespaces(self): - self.getPage('/foo/silly') - self.assertHeader('X-silly', 'sillyval') - self.assertBody('Hello world') - - def testCustomNamespaces(self): - self.getPage('/raw/incr?num=12') - self.assertBody('13') - - self.getPage('/dbscheme') - self.assertBody(r'sqlite///memory') - - def testHandlerToolConfigOverride(self): - # Assert that config overrides tool constructor args. Above, we set - # the favicon in the page handler to be '../favicon.ico', - # but then overrode it in config to be './static/dirback.jpg'. - self.getPage('/favicon.ico') - self.assertBody(open(os.path.join(localDir, 'static/dirback.jpg'), - 'rb').read()) - - def test_request_body_namespace(self): - self.getPage('/plain', method='POST', headers=[ - ('Content-Type', 'application/x-www-form-urlencoded'), - ('Content-Length', '13')], - body=b'\xff\xfex\x00=\xff\xfea\x00b\x00c\x00') - self.assertBody('abc') - - -class VariableSubstitutionTests(unittest.TestCase): - setup_server = staticmethod(setup_server) - - def test_config(self): - from textwrap import dedent - - # variable substitution with [DEFAULT] - conf = dedent(""" - [DEFAULT] - dir = "/some/dir" - my.dir = %(dir)s + "/sub" - - [my] - my.dir = %(dir)s + "/my/dir" - my.dir2 = %(my.dir)s + '/dir2' - - """) - - fp = StringIOFromNative(conf) - - cherrypy.config.update(fp) - self.assertEqual(cherrypy.config['my']['my.dir'], '/some/dir/my/dir') - self.assertEqual(cherrypy.config['my'] - ['my.dir2'], '/some/dir/my/dir/dir2') - - -class CallablesInConfigTest(unittest.TestCase): - setup_server = staticmethod(setup_server) - - def test_call_with_literal_dict(self): - from textwrap import dedent - conf = dedent(""" - [my] - value = dict(**{'foo': 'bar'}) - """) - fp = StringIOFromNative(conf) - cherrypy.config.update(fp) - self.assertEqual(cherrypy.config['my']['value'], {'foo': 'bar'}) - - def test_call_with_kwargs(self): - from textwrap import dedent - conf = dedent(""" - [my] - value = dict(foo="buzz", **cherrypy._test_dict) - """) - test_dict = { - 'foo': 'bar', - 'bar': 'foo', - 'fizz': 'buzz' - } - cherrypy._test_dict = test_dict - fp = StringIOFromNative(conf) - cherrypy.config.update(fp) - test_dict['foo'] = 'buzz' - self.assertEqual(cherrypy.config['my']['value']['foo'], 'buzz') - self.assertEqual(cherrypy.config['my']['value'], test_dict) - del cherrypy._test_dict diff --git a/resources/lib/cherrypy/test/test_config_server.py b/resources/lib/cherrypy/test/test_config_server.py deleted file mode 100644 index 7b18353..0000000 --- a/resources/lib/cherrypy/test/test_config_server.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Tests for the CherryPy configuration system.""" - -import os - -import cherrypy -from cherrypy.test import helper - - -localDir = os.path.join(os.getcwd(), os.path.dirname(__file__)) - - -# Client-side code # - - -class ServerConfigTests(helper.CPWebCase): - - @staticmethod - def setup_server(): - - class Root: - - @cherrypy.expose - def index(self): - return cherrypy.request.wsgi_environ['SERVER_PORT'] - - @cherrypy.expose - def upload(self, file): - return 'Size: %s' % len(file.file.read()) - - @cherrypy.expose - @cherrypy.config(**{'request.body.maxbytes': 100}) - def tinyupload(self): - return cherrypy.request.body.read() - - cherrypy.tree.mount(Root()) - - cherrypy.config.update({ - 'server.socket_host': '0.0.0.0', - 'server.socket_port': 9876, - 'server.max_request_body_size': 200, - 'server.max_request_header_size': 500, - 'server.socket_timeout': 0.5, - - # Test explicit server.instance - 'server.2.instance': 'cherrypy._cpwsgi_server.CPWSGIServer', - 'server.2.socket_port': 9877, - - # Test non-numeric - # Also test default server.instance = builtin server - 'server.yetanother.socket_port': 9878, - }) - - PORT = 9876 - - def testBasicConfig(self): - self.getPage('/') - self.assertBody(str(self.PORT)) - - def testAdditionalServers(self): - if self.scheme == 'https': - return self.skip('not available under ssl') - self.PORT = 9877 - self.getPage('/') - self.assertBody(str(self.PORT)) - self.PORT = 9878 - self.getPage('/') - self.assertBody(str(self.PORT)) - - def testMaxRequestSizePerHandler(self): - if getattr(cherrypy.server, 'using_apache', False): - return self.skip('skipped due to known Apache differences... ') - - self.getPage('/tinyupload', method='POST', - headers=[('Content-Type', 'text/plain'), - ('Content-Length', '100')], - body='x' * 100) - self.assertStatus(200) - self.assertBody('x' * 100) - - self.getPage('/tinyupload', method='POST', - headers=[('Content-Type', 'text/plain'), - ('Content-Length', '101')], - body='x' * 101) - self.assertStatus(413) - - def testMaxRequestSize(self): - if getattr(cherrypy.server, 'using_apache', False): - return self.skip('skipped due to known Apache differences... ') - - for size in (500, 5000, 50000): - self.getPage('/', headers=[('From', 'x' * 500)]) - self.assertStatus(413) - - # Test for https://github.com/cherrypy/cherrypy/issues/421 - # (Incorrect border condition in readline of SizeCheckWrapper). - # This hangs in rev 891 and earlier. - lines256 = 'x' * 248 - self.getPage('/', - headers=[('Host', '%s:%s' % (self.HOST, self.PORT)), - ('From', lines256)]) - - # Test upload - cd = ( - 'Content-Disposition: form-data; ' - 'name="file"; ' - 'filename="hello.txt"' - ) - body = '\r\n'.join([ - '--x', - cd, - 'Content-Type: text/plain', - '', - '%s', - '--x--']) - partlen = 200 - len(body) - b = body % ('x' * partlen) - h = [('Content-type', 'multipart/form-data; boundary=x'), - ('Content-Length', '%s' % len(b))] - self.getPage('/upload', h, 'POST', b) - self.assertBody('Size: %d' % partlen) - - b = body % ('x' * 200) - h = [('Content-type', 'multipart/form-data; boundary=x'), - ('Content-Length', '%s' % len(b))] - self.getPage('/upload', h, 'POST', b) - self.assertStatus(413) diff --git a/resources/lib/cherrypy/test/test_conn.py b/resources/lib/cherrypy/test/test_conn.py deleted file mode 100644 index 1ed1f8d..0000000 --- a/resources/lib/cherrypy/test/test_conn.py +++ /dev/null @@ -1,867 +0,0 @@ -"""Tests for TCP connection handling, including proper and timely close.""" - -import errno -import socket -import sys -import time -import urllib.parse -from http.client import BadStatusLine, HTTPConnection, NotConnected - -import pytest - -from cheroot.test import webtest - -import cherrypy -from cherrypy._cpcompat import HTTPSConnection, ntob, tonative -from cherrypy.test import helper - - -timeout = 1 -pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN' - - -def setup_server(): - - def raise500(): - raise cherrypy.HTTPError(500) - - class Root: - - @cherrypy.expose - def index(self): - return pov - page1 = index - page2 = index - page3 = index - - @cherrypy.expose - def hello(self): - return 'Hello, world!' - - @cherrypy.expose - def timeout(self, t): - return str(cherrypy.server.httpserver.timeout) - - @cherrypy.expose - @cherrypy.config(**{'response.stream': True}) - def stream(self, set_cl=False): - if set_cl: - cherrypy.response.headers['Content-Length'] = 10 - - def content(): - for x in range(10): - yield str(x) - - return content() - - @cherrypy.expose - def error(self, code=500): - raise cherrypy.HTTPError(code) - - @cherrypy.expose - def upload(self): - if not cherrypy.request.method == 'POST': - raise AssertionError("'POST' != request.method %r" % - cherrypy.request.method) - return "thanks for '%s'" % cherrypy.request.body.read() - - @cherrypy.expose - def custom(self, response_code): - cherrypy.response.status = response_code - return 'Code = %s' % response_code - - @cherrypy.expose - @cherrypy.config(**{'hooks.on_start_resource': raise500}) - def err_before_read(self): - return 'ok' - - @cherrypy.expose - def one_megabyte_of_a(self): - return ['a' * 1024] * 1024 - - @cherrypy.expose - # Turn off the encoding tool so it doens't collapse - # our response body and reclaculate the Content-Length. - @cherrypy.config(**{'tools.encode.on': False}) - def custom_cl(self, body, cl): - cherrypy.response.headers['Content-Length'] = cl - if not isinstance(body, list): - body = [body] - newbody = [] - for chunk in body: - if isinstance(chunk, str): - chunk = chunk.encode('ISO-8859-1') - newbody.append(chunk) - return newbody - - cherrypy.tree.mount(Root()) - cherrypy.config.update({ - 'server.max_request_body_size': 1001, - 'server.socket_timeout': timeout, - }) - - -class ConnectionCloseTests(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_HTTP11(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': - return self.skip() - - self.PROTOCOL = 'HTTP/1.1' - - self.persistent = True - - # Make the first request and assert there's no "Connection: close". - self.getPage('/') - self.assertStatus('200 OK') - self.assertBody(pov) - self.assertNoHeader('Connection') - - # Make another request on the same connection. - self.getPage('/page1') - self.assertStatus('200 OK') - self.assertBody(pov) - self.assertNoHeader('Connection') - - # Test client-side close. - self.getPage('/page2', headers=[('Connection', 'close')]) - self.assertStatus('200 OK') - self.assertBody(pov) - self.assertHeader('Connection', 'close') - - # Make another request on the same connection, which should error. - self.assertRaises(NotConnected, self.getPage, '/') - - def test_Streaming_no_len(self): - try: - self._streaming(set_cl=False) - finally: - try: - self.HTTP_CONN.close() - except (TypeError, AttributeError): - pass - - def test_Streaming_with_len(self): - try: - self._streaming(set_cl=True) - finally: - try: - self.HTTP_CONN.close() - except (TypeError, AttributeError): - pass - - def _streaming(self, set_cl): - if cherrypy.server.protocol_version == 'HTTP/1.1': - self.PROTOCOL = 'HTTP/1.1' - - self.persistent = True - - # Make the first request and assert there's no "Connection: close". - self.getPage('/') - self.assertStatus('200 OK') - self.assertBody(pov) - self.assertNoHeader('Connection') - - # Make another, streamed request on the same connection. - if set_cl: - # When a Content-Length is provided, the content should stream - # without closing the connection. - self.getPage('/stream?set_cl=Yes') - self.assertHeader('Content-Length') - self.assertNoHeader('Connection', 'close') - self.assertNoHeader('Transfer-Encoding') - - self.assertStatus('200 OK') - self.assertBody('0123456789') - else: - # When no Content-Length response header is provided, - # streamed output will either close the connection, or use - # chunked encoding, to determine transfer-length. - self.getPage('/stream') - self.assertNoHeader('Content-Length') - self.assertStatus('200 OK') - self.assertBody('0123456789') - - chunked_response = False - for k, v in self.headers: - if k.lower() == 'transfer-encoding': - if str(v) == 'chunked': - chunked_response = True - - if chunked_response: - self.assertNoHeader('Connection', 'close') - else: - self.assertHeader('Connection', 'close') - - # Make another request on the same connection, which should - # error. - self.assertRaises(NotConnected, self.getPage, '/') - - # Try HEAD. See - # https://github.com/cherrypy/cherrypy/issues/864. - self.getPage('/stream', method='HEAD') - self.assertStatus('200 OK') - self.assertBody('') - self.assertNoHeader('Transfer-Encoding') - else: - self.PROTOCOL = 'HTTP/1.0' - - self.persistent = True - - # Make the first request and assert Keep-Alive. - self.getPage('/', headers=[('Connection', 'Keep-Alive')]) - self.assertStatus('200 OK') - self.assertBody(pov) - self.assertHeader('Connection', 'Keep-Alive') - - # Make another, streamed request on the same connection. - if set_cl: - # When a Content-Length is provided, the content should - # stream without closing the connection. - self.getPage('/stream?set_cl=Yes', - headers=[('Connection', 'Keep-Alive')]) - self.assertHeader('Content-Length') - self.assertHeader('Connection', 'Keep-Alive') - self.assertNoHeader('Transfer-Encoding') - self.assertStatus('200 OK') - self.assertBody('0123456789') - else: - # When a Content-Length is not provided, - # the server should close the connection. - self.getPage('/stream', headers=[('Connection', 'Keep-Alive')]) - self.assertStatus('200 OK') - self.assertBody('0123456789') - - self.assertNoHeader('Content-Length') - self.assertNoHeader('Connection', 'Keep-Alive') - self.assertNoHeader('Transfer-Encoding') - - # Make another request on the same connection, which should - # error. - self.assertRaises(NotConnected, self.getPage, '/') - - def test_HTTP10_KeepAlive(self): - self.PROTOCOL = 'HTTP/1.0' - if self.scheme == 'https': - self.HTTP_CONN = HTTPSConnection - else: - self.HTTP_CONN = HTTPConnection - - # Test a normal HTTP/1.0 request. - self.getPage('/page2') - self.assertStatus('200 OK') - self.assertBody(pov) - # Apache, for example, may emit a Connection header even for HTTP/1.0 - # self.assertNoHeader("Connection") - - # Test a keep-alive HTTP/1.0 request. - self.persistent = True - - self.getPage('/page3', headers=[('Connection', 'Keep-Alive')]) - self.assertStatus('200 OK') - self.assertBody(pov) - self.assertHeader('Connection', 'Keep-Alive') - - # Remove the keep-alive header again. - self.getPage('/page3') - self.assertStatus('200 OK') - self.assertBody(pov) - # Apache, for example, may emit a Connection header even for HTTP/1.0 - # self.assertNoHeader("Connection") - - -class PipelineTests(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_HTTP11_Timeout(self): - # If we timeout without sending any data, - # the server will close the conn with a 408. - if cherrypy.server.protocol_version != 'HTTP/1.1': - return self.skip() - - self.PROTOCOL = 'HTTP/1.1' - - # Connect but send nothing. - self.persistent = True - conn = self.HTTP_CONN - conn.auto_open = False - conn.connect() - - # Wait for our socket timeout - time.sleep(timeout * 2) - - # The request should have returned 408 already. - response = conn.response_class(conn.sock, method='GET') - response.begin() - self.assertEqual(response.status, 408) - conn.close() - - # Connect but send half the headers only. - self.persistent = True - conn = self.HTTP_CONN - conn.auto_open = False - conn.connect() - conn.send(b'GET /hello HTTP/1.1') - conn.send(('Host: %s' % self.HOST).encode('ascii')) - - # Wait for our socket timeout - time.sleep(timeout * 2) - - # The conn should have already sent 408. - response = conn.response_class(conn.sock, method='GET') - response.begin() - self.assertEqual(response.status, 408) - conn.close() - - def test_HTTP11_Timeout_after_request(self): - # If we timeout after at least one request has succeeded, - # the server will close the conn without 408. - if cherrypy.server.protocol_version != 'HTTP/1.1': - return self.skip() - - self.PROTOCOL = 'HTTP/1.1' - - # Make an initial request - self.persistent = True - conn = self.HTTP_CONN - conn.putrequest('GET', '/timeout?t=%s' % timeout, skip_host=True) - conn.putheader('Host', self.HOST) - conn.endheaders() - response = conn.response_class(conn.sock, method='GET') - response.begin() - self.assertEqual(response.status, 200) - self.body = response.read() - self.assertBody(str(timeout)) - - # Make a second request on the same socket - conn._output(b'GET /hello HTTP/1.1') - conn._output(ntob('Host: %s' % self.HOST, 'ascii')) - conn._send_output() - response = conn.response_class(conn.sock, method='GET') - response.begin() - self.assertEqual(response.status, 200) - self.body = response.read() - self.assertBody('Hello, world!') - - # Wait for our socket timeout - time.sleep(timeout * 2) - - # Make another request on the same socket, which should error - conn._output(b'GET /hello HTTP/1.1') - conn._output(ntob('Host: %s' % self.HOST, 'ascii')) - conn._send_output() - response = conn.response_class(conn.sock, method='GET') - msg = ( - "Writing to timed out socket didn't fail as it should have: %s") - try: - response.begin() - except Exception: - if not isinstance(sys.exc_info()[1], - (socket.error, BadStatusLine)): - self.fail(msg % sys.exc_info()[1]) - else: - if response.status != 408: - self.fail(msg % response.read()) - - conn.close() - - # Make another request on a new socket, which should work - self.persistent = True - conn = self.HTTP_CONN - conn.putrequest('GET', '/', skip_host=True) - conn.putheader('Host', self.HOST) - conn.endheaders() - response = conn.response_class(conn.sock, method='GET') - response.begin() - self.assertEqual(response.status, 200) - self.body = response.read() - self.assertBody(pov) - - # Make another request on the same socket, - # but timeout on the headers - conn.send(b'GET /hello HTTP/1.1') - # Wait for our socket timeout - time.sleep(timeout * 2) - response = conn.response_class(conn.sock, method='GET') - try: - response.begin() - except Exception: - if not isinstance(sys.exc_info()[1], - (socket.error, BadStatusLine)): - self.fail(msg % sys.exc_info()[1]) - else: - if response.status != 408: - self.fail(msg % response.read()) - - conn.close() - - # Retry the request on a new connection, which should work - self.persistent = True - conn = self.HTTP_CONN - conn.putrequest('GET', '/', skip_host=True) - conn.putheader('Host', self.HOST) - conn.endheaders() - response = conn.response_class(conn.sock, method='GET') - response.begin() - self.assertEqual(response.status, 200) - self.body = response.read() - self.assertBody(pov) - conn.close() - - def test_HTTP11_pipelining(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': - return self.skip() - - self.PROTOCOL = 'HTTP/1.1' - - # Test pipelining. httplib doesn't support this directly. - self.persistent = True - conn = self.HTTP_CONN - - # Put request 1 - conn.putrequest('GET', '/hello', skip_host=True) - conn.putheader('Host', self.HOST) - conn.endheaders() - - for trial in range(5): - # Put next request - conn._output(b'GET /hello HTTP/1.1') - conn._output(ntob('Host: %s' % self.HOST, 'ascii')) - conn._send_output() - - # Retrieve previous response - response = conn.response_class(conn.sock, method='GET') - # there is a bug in python3 regarding the buffering of - # ``conn.sock``. Until that bug get's fixed we will - # monkey patch the ``response`` instance. - # https://bugs.python.org/issue23377 - response.fp = conn.sock.makefile('rb', 0) - response.begin() - body = response.read(13) - self.assertEqual(response.status, 200) - self.assertEqual(body, b'Hello, world!') - - # Retrieve final response - response = conn.response_class(conn.sock, method='GET') - response.begin() - body = response.read() - self.assertEqual(response.status, 200) - self.assertEqual(body, b'Hello, world!') - - conn.close() - - def test_100_Continue(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': - return self.skip() - - self.PROTOCOL = 'HTTP/1.1' - - self.persistent = True - conn = self.HTTP_CONN - - # Try a page without an Expect request header first. - # Note that httplib's response.begin automatically ignores - # 100 Continue responses, so we must manually check for it. - try: - conn.putrequest('POST', '/upload', skip_host=True) - conn.putheader('Host', self.HOST) - conn.putheader('Content-Type', 'text/plain') - conn.putheader('Content-Length', '4') - conn.endheaders() - conn.send(ntob("d'oh")) - response = conn.response_class(conn.sock, method='POST') - version, status, reason = response._read_status() - self.assertNotEqual(status, 100) - finally: - conn.close() - - # Now try a page with an Expect header... - try: - conn.connect() - conn.putrequest('POST', '/upload', skip_host=True) - conn.putheader('Host', self.HOST) - conn.putheader('Content-Type', 'text/plain') - conn.putheader('Content-Length', '17') - conn.putheader('Expect', '100-continue') - conn.endheaders() - response = conn.response_class(conn.sock, method='POST') - - # ...assert and then skip the 100 response - version, status, reason = response._read_status() - self.assertEqual(status, 100) - while True: - line = response.fp.readline().strip() - if line: - self.fail( - '100 Continue should not output any headers. Got %r' % - line) - else: - break - - # ...send the body - body = b'I am a small file' - conn.send(body) - - # ...get the final response - response.begin() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus(200) - self.assertBody("thanks for '%s'" % body) - finally: - conn.close() - - -class ConnectionTests(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_readall_or_close(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': - return self.skip() - - self.PROTOCOL = 'HTTP/1.1' - - if self.scheme == 'https': - self.HTTP_CONN = HTTPSConnection - else: - self.HTTP_CONN = HTTPConnection - - # Test a max of 0 (the default) and then reset to what it was above. - old_max = cherrypy.server.max_request_body_size - for new_max in (0, old_max): - cherrypy.server.max_request_body_size = new_max - - self.persistent = True - conn = self.HTTP_CONN - - # Get a POST page with an error - conn.putrequest('POST', '/err_before_read', skip_host=True) - conn.putheader('Host', self.HOST) - conn.putheader('Content-Type', 'text/plain') - conn.putheader('Content-Length', '1000') - conn.putheader('Expect', '100-continue') - conn.endheaders() - response = conn.response_class(conn.sock, method='POST') - - # ...assert and then skip the 100 response - version, status, reason = response._read_status() - self.assertEqual(status, 100) - while True: - skip = response.fp.readline().strip() - if not skip: - break - - # ...send the body - conn.send(ntob('x' * 1000)) - - # ...get the final response - response.begin() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus(500) - - # Now try a working page with an Expect header... - conn._output(b'POST /upload HTTP/1.1') - conn._output(ntob('Host: %s' % self.HOST, 'ascii')) - conn._output(b'Content-Type: text/plain') - conn._output(b'Content-Length: 17') - conn._output(b'Expect: 100-continue') - conn._send_output() - response = conn.response_class(conn.sock, method='POST') - - # ...assert and then skip the 100 response - version, status, reason = response._read_status() - self.assertEqual(status, 100) - while True: - skip = response.fp.readline().strip() - if not skip: - break - - # ...send the body - body = b'I am a small file' - conn.send(body) - - # ...get the final response - response.begin() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus(200) - self.assertBody("thanks for '%s'" % body) - conn.close() - - def test_No_Message_Body(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': - return self.skip() - - self.PROTOCOL = 'HTTP/1.1' - - # Set our HTTP_CONN to an instance so it persists between requests. - self.persistent = True - - # Make the first request and assert there's no "Connection: close". - self.getPage('/') - self.assertStatus('200 OK') - self.assertBody(pov) - self.assertNoHeader('Connection') - - # Make a 204 request on the same connection. - self.getPage('/custom/204') - self.assertStatus(204) - self.assertNoHeader('Content-Length') - self.assertBody('') - self.assertNoHeader('Connection') - - # Make a 304 request on the same connection. - self.getPage('/custom/304') - self.assertStatus(304) - self.assertNoHeader('Content-Length') - self.assertBody('') - self.assertNoHeader('Connection') - - def test_Chunked_Encoding(self): - if cherrypy.server.protocol_version != 'HTTP/1.1': - return self.skip() - - if (hasattr(self, 'harness') and - 'modpython' in self.harness.__class__.__name__.lower()): - # mod_python forbids chunked encoding - return self.skip() - - self.PROTOCOL = 'HTTP/1.1' - - # Set our HTTP_CONN to an instance so it persists between requests. - self.persistent = True - conn = self.HTTP_CONN - - # Try a normal chunked request (with extensions) - body = ntob('8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n' - 'Content-Type: application/json\r\n' - '\r\n') - conn.putrequest('POST', '/upload', skip_host=True) - conn.putheader('Host', self.HOST) - conn.putheader('Transfer-Encoding', 'chunked') - conn.putheader('Trailer', 'Content-Type') - # Note that this is somewhat malformed: - # we shouldn't be sending Content-Length. - # RFC 2616 says the server should ignore it. - conn.putheader('Content-Length', '3') - conn.endheaders() - conn.send(body) - response = conn.getresponse() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus('200 OK') - self.assertBody("thanks for '%s'" % b'xx\r\nxxxxyyyyy') - - # Try a chunked request that exceeds server.max_request_body_size. - # Note that the delimiters and trailer are included. - body = ntob('3e3\r\n' + ('x' * 995) + '\r\n0\r\n\r\n') - conn.putrequest('POST', '/upload', skip_host=True) - conn.putheader('Host', self.HOST) - conn.putheader('Transfer-Encoding', 'chunked') - conn.putheader('Content-Type', 'text/plain') - # Chunked requests don't need a content-length - # # conn.putheader("Content-Length", len(body)) - conn.endheaders() - conn.send(body) - response = conn.getresponse() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus(413) - conn.close() - - def test_Content_Length_in(self): - # Try a non-chunked request where Content-Length exceeds - # server.max_request_body_size. Assert error before body send. - self.persistent = True - conn = self.HTTP_CONN - conn.putrequest('POST', '/upload', skip_host=True) - conn.putheader('Host', self.HOST) - conn.putheader('Content-Type', 'text/plain') - conn.putheader('Content-Length', '9999') - conn.endheaders() - response = conn.getresponse() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus(413) - self.assertBody('The entity sent with the request exceeds ' - 'the maximum allowed bytes.') - conn.close() - - def test_Content_Length_out_preheaders(self): - # Try a non-chunked response where Content-Length is less than - # the actual bytes in the response body. - self.persistent = True - conn = self.HTTP_CONN - conn.putrequest('GET', '/custom_cl?body=I+have+too+many+bytes&cl=5', - skip_host=True) - conn.putheader('Host', self.HOST) - conn.endheaders() - response = conn.getresponse() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus(500) - self.assertBody( - 'The requested resource returned more bytes than the ' - 'declared Content-Length.') - conn.close() - - def test_Content_Length_out_postheaders(self): - # Try a non-chunked response where Content-Length is less than - # the actual bytes in the response body. - self.persistent = True - conn = self.HTTP_CONN - conn.putrequest( - 'GET', '/custom_cl?body=I+too&body=+have+too+many&cl=5', - skip_host=True) - conn.putheader('Host', self.HOST) - conn.endheaders() - response = conn.getresponse() - self.status, self.headers, self.body = webtest.shb(response) - self.assertStatus(200) - self.assertBody('I too') - conn.close() - - def test_598(self): - tmpl = '{scheme}://{host}:{port}/one_megabyte_of_a/' - url = tmpl.format( - scheme=self.scheme, - host=self.HOST, - port=self.PORT, - ) - remote_data_conn = urllib.request.urlopen(url) - buf = remote_data_conn.read(512) - time.sleep(timeout * 0.6) - remaining = (1024 * 1024) - 512 - while remaining: - data = remote_data_conn.read(remaining) - if not data: - break - else: - buf += data - remaining -= len(data) - - self.assertEqual(len(buf), 1024 * 1024) - self.assertEqual(buf, ntob('a' * 1024 * 1024)) - self.assertEqual(remaining, 0) - remote_data_conn.close() - - -def setup_upload_server(): - - class Root: - @cherrypy.expose - def upload(self): - if not cherrypy.request.method == 'POST': - raise AssertionError("'POST' != request.method %r" % - cherrypy.request.method) - return "thanks for '%s'" % tonative(cherrypy.request.body.read()) - - cherrypy.tree.mount(Root()) - cherrypy.config.update({ - 'server.max_request_body_size': 1001, - 'server.socket_timeout': 10, - 'server.accepted_queue_size': 5, - 'server.accepted_queue_timeout': 0.1, - }) - - -reset_names = 'ECONNRESET', 'WSAECONNRESET' -socket_reset_errors = [ - getattr(errno, name) - for name in reset_names - if hasattr(errno, name) -] -'reset error numbers available on this platform' - -socket_reset_errors += [ - # Python 3.5 raises an http.client.RemoteDisconnected - # with this message - 'Remote end closed connection without response', -] - - -class LimitedRequestQueueTests(helper.CPWebCase): - setup_server = staticmethod(setup_upload_server) - - @pytest.mark.xfail(reason='#1535') - def test_queue_full(self): - conns = [] - overflow_conn = None - - try: - # Make 15 initial requests and leave them open, which should use - # all of wsgiserver's WorkerThreads and fill its Queue. - for i in range(15): - conn = self.HTTP_CONN(self.HOST, self.PORT) - conn.putrequest('POST', '/upload', skip_host=True) - conn.putheader('Host', self.HOST) - conn.putheader('Content-Type', 'text/plain') - conn.putheader('Content-Length', '4') - conn.endheaders() - conns.append(conn) - - # Now try a 16th conn, which should be closed by the - # server immediately. - overflow_conn = self.HTTP_CONN(self.HOST, self.PORT) - # Manually connect since httplib won't let us set a timeout - for res in socket.getaddrinfo(self.HOST, self.PORT, 0, - socket.SOCK_STREAM): - af, socktype, proto, canonname, sa = res - overflow_conn.sock = socket.socket(af, socktype, proto) - overflow_conn.sock.settimeout(5) - overflow_conn.sock.connect(sa) - break - - overflow_conn.putrequest('GET', '/', skip_host=True) - overflow_conn.putheader('Host', self.HOST) - overflow_conn.endheaders() - response = overflow_conn.response_class( - overflow_conn.sock, - method='GET', - ) - try: - response.begin() - except socket.error as exc: - if exc.args[0] in socket_reset_errors: - pass # Expected. - else: - tmpl = ( - 'Overflow conn did not get RST. ' - 'Got {exc.args!r} instead' - ) - raise AssertionError(tmpl.format(**locals())) - except BadStatusLine: - # This is a special case in OS X. Linux and Windows will - # RST correctly. - assert sys.platform == 'darwin' - else: - raise AssertionError('Overflow conn did not get RST ') - finally: - for conn in conns: - conn.send(b'done') - response = conn.response_class(conn.sock, method='POST') - response.begin() - self.body = response.read() - self.assertBody("thanks for 'done'") - self.assertEqual(response.status, 200) - conn.close() - if overflow_conn: - overflow_conn.close() - - -class BadRequestTests(helper.CPWebCase): - setup_server = staticmethod(setup_server) - - def test_No_CRLF(self): - self.persistent = True - - conn = self.HTTP_CONN - conn.send(b'GET /hello HTTP/1.1\n\n') - response = conn.response_class(conn.sock, method='GET') - response.begin() - self.body = response.read() - self.assertBody('HTTP requires CRLF terminators') - conn.close() - - conn.connect() - conn.send(b'GET /hello HTTP/1.1\r\n\n') - response = conn.response_class(conn.sock, method='GET') - response.begin() - self.body = response.read() - self.assertBody('HTTP requires CRLF terminators') - conn.close() diff --git a/resources/lib/cherrypy/test/test_core.py b/resources/lib/cherrypy/test/test_core.py deleted file mode 100644 index 6fde3a9..0000000 --- a/resources/lib/cherrypy/test/test_core.py +++ /dev/null @@ -1,826 +0,0 @@ -# coding: utf-8 - -"""Basic tests for the CherryPy core: request handling.""" - -import os -import sys -import types - -import cherrypy -from cherrypy._cpcompat import ntou -from cherrypy import _cptools, tools -from cherrypy.lib import httputil, static - -from cherrypy.test._test_decorators import ExposeExamples -from cherrypy.test import helper - - -localDir = os.path.dirname(__file__) -favicon_path = os.path.join(os.getcwd(), localDir, '../favicon.ico') - -# Client-side code # - - -class CoreRequestHandlingTest(helper.CPWebCase): - - @staticmethod - def setup_server(): - class Root: - - @cherrypy.expose - def index(self): - return 'hello' - - favicon_ico = tools.staticfile.handler(filename=favicon_path) - - @cherrypy.expose - def defct(self, newct): - newct = 'text/%s' % newct - cherrypy.config.update({'tools.response_headers.on': True, - 'tools.response_headers.headers': - [('Content-Type', newct)]}) - - @cherrypy.expose - def baseurl(self, path_info, relative=None): - return cherrypy.url(path_info, relative=bool(relative)) - - root = Root() - root.expose_dec = ExposeExamples() - - class TestType(type): - - """Metaclass which automatically exposes all functions in each - subclass, and adds an instance of the subclass as an attribute - of root. - """ - def __init__(cls, name, bases, dct): - type.__init__(cls, name, bases, dct) - for value in dct.values(): - if isinstance(value, types.FunctionType): - value.exposed = True - setattr(root, name.lower(), cls()) - Test = TestType('Test', (object, ), {}) - - @cherrypy.config(**{'tools.trailing_slash.on': False}) - class URL(Test): - - def index(self, path_info, relative=None): - if relative != 'server': - relative = bool(relative) - return cherrypy.url(path_info, relative=relative) - - def leaf(self, path_info, relative=None): - if relative != 'server': - relative = bool(relative) - return cherrypy.url(path_info, relative=relative) - - def qs(self, qs): - return cherrypy.url(qs=qs) - - def log_status(): - Status.statuses.append(cherrypy.response.status) - cherrypy.tools.log_status = cherrypy.Tool( - 'on_end_resource', log_status) - - class Status(Test): - - def index(self): - return 'normal' - - def blank(self): - cherrypy.response.status = '' - - # According to RFC 2616, new status codes are OK as long as they - # are between 100 and 599. - - # Here is an illegal code... - def illegal(self): - cherrypy.response.status = 781 - return 'oops' - - # ...and here is an unknown but legal code. - def unknown(self): - cherrypy.response.status = '431 My custom error' - return 'funky' - - # Non-numeric code - def bad(self): - cherrypy.response.status = 'error' - return 'bad news' - - statuses = [] - - @cherrypy.config(**{'tools.log_status.on': True}) - def on_end_resource_stage(self): - return repr(self.statuses) - - class Redirect(Test): - - @cherrypy.config(**{ - 'tools.err_redirect.on': True, - 'tools.err_redirect.url': '/errpage', - 'tools.err_redirect.internal': False, - }) - class Error: - @cherrypy.expose - def index(self): - raise NameError('redirect_test') - - error = Error() - - def index(self): - return 'child' - - def custom(self, url, code): - raise cherrypy.HTTPRedirect(url, code) - - @cherrypy.config(**{'tools.trailing_slash.extra': True}) - def by_code(self, code): - raise cherrypy.HTTPRedirect('somewhere%20else', code) - - def nomodify(self): - raise cherrypy.HTTPRedirect('', 304) - - def proxy(self): - raise cherrypy.HTTPRedirect('proxy', 305) - - def stringify(self): - return str(cherrypy.HTTPRedirect('/')) - - def fragment(self, frag): - raise cherrypy.HTTPRedirect('/some/url#%s' % frag) - - def url_with_quote(self): - raise cherrypy.HTTPRedirect("/some\"url/that'we/want") - - def url_with_xss(self): - raise cherrypy.HTTPRedirect( - "/someurl/that'we/want") - - def url_with_unicode(self): - raise cherrypy.HTTPRedirect(ntou('тест', 'utf-8')) - - def login_redir(): - if not getattr(cherrypy.request, 'login', None): - raise cherrypy.InternalRedirect('/internalredirect/login') - tools.login_redir = _cptools.Tool('before_handler', login_redir) - - def redir_custom(): - raise cherrypy.InternalRedirect('/internalredirect/custom_err') - - class InternalRedirect(Test): - - def index(self): - raise cherrypy.InternalRedirect('/') - - @cherrypy.expose - @cherrypy.config(**{'hooks.before_error_response': redir_custom}) - def choke(self): - return 3 / 0 - - def relative(self, a, b): - raise cherrypy.InternalRedirect('cousin?t=6') - - def cousin(self, t): - assert cherrypy.request.prev.closed - return cherrypy.request.prev.query_string - - def petshop(self, user_id): - if user_id == 'parrot': - # Trade it for a slug when redirecting - raise cherrypy.InternalRedirect( - '/image/getImagesByUser?user_id=slug') - elif user_id == 'terrier': - # Trade it for a fish when redirecting - raise cherrypy.InternalRedirect( - '/image/getImagesByUser?user_id=fish') - else: - # This should pass the user_id through to getImagesByUser - raise cherrypy.InternalRedirect( - '/image/getImagesByUser?user_id=%s' % str(user_id)) - - # We support Python 2.3, but the @-deco syntax would look like - # this: - # @tools.login_redir() - def secure(self): - return 'Welcome!' - secure = tools.login_redir()(secure) - # Since calling the tool returns the same function you pass in, - # you could skip binding the return value, and just write: - # tools.login_redir()(secure) - - def login(self): - return 'Please log in' - - def custom_err(self): - return 'Something went horribly wrong.' - - @cherrypy.config(**{'hooks.before_request_body': redir_custom}) - def early_ir(self, arg): - return 'whatever' - - class Image(Test): - - def getImagesByUser(self, user_id): - return '0 images for %s' % user_id - - class Flatten(Test): - - def as_string(self): - return 'content' - - def as_list(self): - return ['con', 'tent'] - - def as_yield(self): - yield b'content' - - @cherrypy.config(**{'tools.flatten.on': True}) - def as_dblyield(self): - yield self.as_yield() - - def as_refyield(self): - for chunk in self.as_yield(): - yield chunk - - class Ranges(Test): - - def get_ranges(self, bytes): - return repr(httputil.get_ranges('bytes=%s' % bytes, 8)) - - def slice_file(self): - path = os.path.join(os.getcwd(), os.path.dirname(__file__)) - return static.serve_file( - os.path.join(path, 'static/index.html')) - - class Cookies(Test): - - def single(self, name): - cookie = cherrypy.request.cookie[name] - # Python2's SimpleCookie.__setitem__ won't take unicode keys. - cherrypy.response.cookie[str(name)] = cookie.value - - def multiple(self, names): - list(map(self.single, names)) - - def append_headers(header_list, debug=False): - if debug: - cherrypy.log( - 'Extending response headers with %s' % repr(header_list), - 'TOOLS.APPEND_HEADERS') - cherrypy.serving.response.header_list.extend(header_list) - cherrypy.tools.append_headers = cherrypy.Tool( - 'on_end_resource', append_headers) - - class MultiHeader(Test): - - def header_list(self): - pass - header_list = cherrypy.tools.append_headers(header_list=[ - (b'WWW-Authenticate', b'Negotiate'), - (b'WWW-Authenticate', b'Basic realm="foo"'), - ])(header_list) - - def commas(self): - cherrypy.response.headers[ - 'WWW-Authenticate'] = 'Negotiate,Basic realm="foo"' - - cherrypy.tree.mount(root) - - def testStatus(self): - self.getPage('/status/') - self.assertBody('normal') - self.assertStatus(200) - - self.getPage('/status/blank') - self.assertBody('') - self.assertStatus(200) - - self.getPage('/status/illegal') - self.assertStatus(500) - msg = 'Illegal response status from server (781 is out of range).' - self.assertErrorPage(500, msg) - - if not getattr(cherrypy.server, 'using_apache', False): - self.getPage('/status/unknown') - self.assertBody('funky') - self.assertStatus(431) - - self.getPage('/status/bad') - self.assertStatus(500) - msg = "Illegal response status from server ('error' is non-numeric)." - self.assertErrorPage(500, msg) - - def test_on_end_resource_status(self): - self.getPage('/status/on_end_resource_stage') - self.assertBody('[]') - self.getPage('/status/on_end_resource_stage') - self.assertBody(repr(['200 OK'])) - - def testSlashes(self): - # Test that requests for index methods without a trailing slash - # get redirected to the same URI path with a trailing slash. - # Make sure GET params are preserved. - self.getPage('/redirect?id=3') - self.assertStatus(301) - self.assertMatchesBody( - '' - '%s/redirect/[?]id=3' % (self.base(), self.base()) - ) - - if self.prefix(): - # Corner case: the "trailing slash" redirect could be tricky if - # we're using a virtual root and the URI is "/vroot" (no slash). - self.getPage('') - self.assertStatus(301) - self.assertMatchesBody("%s/" % - (self.base(), self.base())) - - # Test that requests for NON-index methods WITH a trailing slash - # get redirected to the same URI path WITHOUT a trailing slash. - # Make sure GET params are preserved. - self.getPage('/redirect/by_code/?code=307') - self.assertStatus(301) - self.assertMatchesBody( - "" - '%s/redirect/by_code[?]code=307' - % (self.base(), self.base()) - ) - - # If the trailing_slash tool is off, CP should just continue - # as if the slashes were correct. But it needs some help - # inside cherrypy.url to form correct output. - self.getPage('/url?path_info=page1') - self.assertBody('%s/url/page1' % self.base()) - self.getPage('/url/leaf/?path_info=page1') - self.assertBody('%s/url/page1' % self.base()) - - def testRedirect(self): - self.getPage('/redirect/') - self.assertBody('child') - self.assertStatus(200) - - self.getPage('/redirect/by_code?code=300') - self.assertMatchesBody( - r"\2somewhere%20else") - self.assertStatus(300) - - self.getPage('/redirect/by_code?code=301') - self.assertMatchesBody( - r"\2somewhere%20else") - self.assertStatus(301) - - self.getPage('/redirect/by_code?code=302') - self.assertMatchesBody( - r"\2somewhere%20else") - self.assertStatus(302) - - self.getPage('/redirect/by_code?code=303') - self.assertMatchesBody( - r"\2somewhere%20else") - self.assertStatus(303) - - self.getPage('/redirect/by_code?code=307') - self.assertMatchesBody( - r"\2somewhere%20else") - self.assertStatus(307) - - self.getPage('/redirect/by_code?code=308') - self.assertMatchesBody( - r"\2somewhere%20else") - self.assertStatus(308) - - self.getPage('/redirect/nomodify') - self.assertBody('') - self.assertStatus(304) - - self.getPage('/redirect/proxy') - self.assertBody('') - self.assertStatus(305) - - # HTTPRedirect on error - self.getPage('/redirect/error/') - self.assertStatus(('302 Found', '303 See Other')) - self.assertInBody('/errpage') - - # Make sure str(HTTPRedirect()) works. - self.getPage('/redirect/stringify', protocol='HTTP/1.0') - self.assertStatus(200) - self.assertBody("(['%s/'], 302)" % self.base()) - if cherrypy.server.protocol_version == 'HTTP/1.1': - self.getPage('/redirect/stringify', protocol='HTTP/1.1') - self.assertStatus(200) - self.assertBody("(['%s/'], 303)" % self.base()) - - # check that #fragments are handled properly - # http://skrb.org/ietf/http_errata.html#location-fragments - frag = 'foo' - self.getPage('/redirect/fragment/%s' % frag) - self.assertMatchesBody( - r"\2\/some\/url\#%s" % ( - frag, frag)) - loc = self.assertHeader('Location') - assert loc.endswith('#%s' % frag) - self.assertStatus(('302 Found', '303 See Other')) - - # check injection protection - # See https://github.com/cherrypy/cherrypy/issues/1003 - self.getPage( - '/redirect/custom?' - 'code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval') - self.assertStatus(303) - loc = self.assertHeader('Location') - assert 'Set-Cookie' in loc - self.assertNoHeader('Set-Cookie') - - def assertValidXHTML(): - from xml.etree import ElementTree - try: - ElementTree.fromstring( - '%s' % self.body, - ) - except ElementTree.ParseError: - self._handlewebError( - 'automatically generated redirect did not ' - 'generate well-formed html', - ) - - # check redirects to URLs generated valid HTML - we check this - # by seeing if it appears as valid XHTML. - self.getPage('/redirect/by_code?code=303') - self.assertStatus(303) - assertValidXHTML() - - # do the same with a url containing quote characters. - self.getPage('/redirect/url_with_quote') - self.assertStatus(303) - assertValidXHTML() - - def test_redirect_with_xss(self): - """A redirect to a URL with HTML injected should result - in page contents escaped.""" - self.getPage('/redirect/url_with_xss') - self.assertStatus(303) - assert b'