diff --git a/README.md b/README.md index 5ca07d6aa..cf8dd3030 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # bazarr -Bazarr is a companion application to Sonarr and Radarr. It can manage and download subtitles based on your requirements. You define your preferences by TV show or movie and Bazarr takes care of everything for you. +Bazarr is a companion application to Sonarr and Radarr. It manages and downloads subtitles based on your requirements. You define your preferences by TV show or movies and Bazarr takes care of everything for you. Be aware that Bazarr doesn't scan disk to detect series and movies: It only takes care of the series and movies that are indexed in Sonarr and Radarr. @@ -45,7 +45,7 @@ If you need something that is not already part of Bazarr, feel free to create a You can get more in the [screenshot](https://github.com/morpheus65535/bazarr/tree/master/screenshot) directory but it should look familiar: -![Series](/screenshot/series.png?raw=true "Series") +![Series](/screenshot/1-series/series-2-episodes.png?raw=true "Series") ### License diff --git a/bazarr/check_update.py b/bazarr/check_update.py index 28c0a31d5..c780b7e1e 100644 --- a/bazarr/check_update.py +++ b/bazarr/check_update.py @@ -3,6 +3,9 @@ import os import logging import sqlite3 +import json +import requests + import git from get_args import args @@ -31,6 +34,7 @@ def gitconfig(): def check_and_apply_update(): gitconfig() + check_releases() branch = get_general_settings()[5] g = git.cmd.Git(current_working_directory) g.fetch('origin') @@ -46,6 +50,27 @@ def check_and_apply_update(): updated() +def check_releases(): + releases = [] + url_releases = 'https://api.github.com/repos/morpheus65535/Bazarr/releases' + try: + r = requests.get(url_releases, timeout=15) + r.raise_for_status() + except requests.exceptions.HTTPError as errh: + logging.exception("Error trying to get releases from Github. Http error.") + except requests.exceptions.ConnectionError as errc: + logging.exception("Error trying to get releases from Github. Connection Error.") + except requests.exceptions.Timeout as errt: + logging.exception("Error trying to get releases from Github. Timeout Error.") + except requests.exceptions.RequestException as err: + logging.exception("Error trying to get releases from Github.") + else: + for release in r.json(): + releases.append([release['name'], release['body']]) + with open(os.path.join(config_dir, 'config', 'releases.txt'), 'w') as f: + json.dump(releases, f) + + def updated(): conn = sqlite3.connect(os.path.join(args.config_dir, 'db', 'bazarr.db'), timeout=30) c = conn.cursor() diff --git a/bazarr/create_db.sql b/bazarr/create_db.sql index 067cac174..dc2188e44 100644 --- a/bazarr/create_db.sql +++ b/bazarr/create_db.sql @@ -28,10 +28,10 @@ CREATE TABLE "table_settings_notifier" ( ); CREATE TABLE "table_settings_languages" ( `code3` TEXT NOT NULL UNIQUE, - `code3b` TEXT, `code2` TEXT, `name` TEXT NOT NULL, `enabled` INTEGER, + `code3b` TEXT, PRIMARY KEY(`code3`) ); CREATE TABLE "table_history" ( diff --git a/bazarr/get_episodes.py b/bazarr/get_episodes.py index 9d29f70ff..d3eecd63c 100644 --- a/bazarr/get_episodes.py +++ b/bazarr/get_episodes.py @@ -112,6 +112,9 @@ def sync_episodes(): # Close database connection c.close() + for updated_episode in episodes_to_update: + store_subtitles(path_replace(updated_episode[1])) + for added_episode in episodes_to_add: store_subtitles(path_replace(added_episode[3])) diff --git a/bazarr/get_movies.py b/bazarr/get_movies.py index 9ddd7cbb6..2da8a0917 100644 --- a/bazarr/get_movies.py +++ b/bazarr/get_movies.py @@ -52,78 +52,60 @@ def update_movies(): for movie in r.json(): if movie['hasFile'] is True: if 'movieFile' in movie: - try: - overview = unicode(movie['overview']) - except: - overview = "" - try: - poster_big = movie['images'][0]['url'] - poster = os.path.splitext(poster_big)[0] + '-500' + os.path.splitext(poster_big)[1] - except: - poster = "" - try: - fanart = movie['images'][1]['url'] - except: - fanart = "" + if movie["path"] != None and movie['movieFile']['relativePath'] != None: + try: + overview = unicode(movie['overview']) + except: + overview = "" + try: + poster_big = movie['images'][0]['url'] + poster = os.path.splitext(poster_big)[0] + '-500' + os.path.splitext(poster_big)[1] + except: + poster = "" + try: + fanart = movie['images'][1]['url'] + except: + fanart = "" - if 'sceneName' in movie['movieFile']: - sceneName = movie['movieFile']['sceneName'] - else: - sceneName = None - - # Add movies in radarr to current movies list - current_movies_radarr.append(unicode(movie['tmdbId'])) - - # Detect file separator - if movie['path'][0] == "/": - separator = "/" - else: - separator = "\\" - - if unicode(movie['tmdbId']) in current_movies_db_list: - movies_to_update.append((movie["title"], - movie["path"] + separator + movie['movieFile']['relativePath'], - movie["tmdbId"], movie["id"], overview, poster, fanart, - profile_id_to_language(movie['qualityProfileId']), sceneName, - unicode(bool(movie['monitored'])), movie["tmdbId"])) - else: - if movie_default_enabled is True: - movies_to_add.append((movie["title"], - movie["path"] + separator + movie['movieFile']['relativePath'], - movie["tmdbId"], movie_default_language, '[]', movie_default_hi, - movie["id"], overview, poster, fanart, - profile_id_to_language(movie['qualityProfileId']), sceneName, - unicode(bool(movie['monitored'])))) + if 'sceneName' in movie['movieFile']: + sceneName = movie['movieFile']['sceneName'] else: - movies_to_add.append((movie["title"], - movie["path"] + separator + movie['movieFile']['relativePath'], - movie["tmdbId"], movie["tmdbId"], movie["tmdbId"], movie["id"], - overview, poster, fanart, - profile_id_to_language(movie['qualityProfileId']), sceneName, - unicode(bool(movie['monitored'])))) + sceneName = None + + # Add movies in radarr to current movies list + current_movies_radarr.append(unicode(movie['tmdbId'])) + + # Detect file separator + if movie['path'][0] == "/": + separator = "/" + else: + separator = "\\" + + if unicode(movie['tmdbId']) in current_movies_db_list: + movies_to_update.append((movie["title"],movie["path"] + separator + movie['movieFile']['relativePath'],movie["tmdbId"],movie["id"],overview,poster,fanart,profile_id_to_language(movie['qualityProfileId']),sceneName,unicode(bool(movie['monitored'])),movie['sortTitle'],movie["tmdbId"])) + else: + if movie_default_enabled is True: + movies_to_add.append((movie["title"], movie["path"] + separator + movie['movieFile']['relativePath'], movie["tmdbId"], movie_default_language, '[]', movie_default_hi, movie["id"], overview, poster, fanart, profile_id_to_language(movie['qualityProfileId']), sceneName, unicode(bool(movie['monitored'])),movie['sortTitle'])) + else: + movies_to_add.append((movie["title"], movie["path"] + separator + movie['movieFile']['relativePath'], movie["tmdbId"], movie["tmdbId"], movie["tmdbId"], movie["id"], overview, poster, fanart, profile_id_to_language(movie['qualityProfileId']), sceneName, unicode(bool(movie['monitored'])),movie['sortTitle'])) + else: + logging.error('BAZARR Radarr returned a movie without a file path: ' + movie["path"] + separator + movie['movieFile']['relativePath']) # Update or insert movies in DB db = sqlite3.connect(os.path.join(args.config_dir, 'db', 'bazarr.db'), timeout=30) c = db.cursor() - updated_result = c.executemany( - '''UPDATE table_movies SET title = ?, path = ?, tmdbId = ?, radarrId = ?, overview = ?, poster = ?, fanart = ?, `audio_language` = ?, sceneName = ?, monitored = ? WHERE tmdbid = ?''', - movies_to_update) + updated_result = c.executemany('''UPDATE table_movies SET title = ?, path = ?, tmdbId = ?, radarrId = ?, overview = ?, poster = ?, fanart = ?, `audio_language` = ?, sceneName = ?, monitored = ?, sortTitle= ? WHERE tmdbid = ?''', movies_to_update) db.commit() if movie_default_enabled is True: - added_result = c.executemany( - '''INSERT OR IGNORE INTO table_movies(title, path, tmdbId, languages, subtitles,`hearing_impaired`, radarrId, overview, poster, fanart, `audio_language`, sceneName, monitored) VALUES (?,?,?,?,?, ?, ?, ?, ?, ?, ?, ?, ?)''', - movies_to_add) + added_result = c.executemany('''INSERT OR IGNORE INTO table_movies(title, path, tmdbId, languages, subtitles,`hearing_impaired`, radarrId, overview, poster, fanart, `audio_language`, sceneName, monitored, sortTitle) VALUES (?,?,?,?,?, ?, ?, ?, ?, ?, ?, ?, ?, ?)''', movies_to_add) db.commit() else: - added_result = c.executemany( - '''INSERT OR IGNORE INTO table_movies(title, path, tmdbId, languages, subtitles,`hearing_impaired`, radarrId, overview, poster, fanart, `audio_language`, sceneName, monitored) VALUES (?,?,?,(SELECT languages FROM table_movies WHERE tmdbId = ?), '[]',(SELECT `hearing_impaired` FROM table_movies WHERE tmdbId = ?), ?, ?, ?, ?, ?, ?, ?)''', - movies_to_add) + added_result = c.executemany('''INSERT OR IGNORE INTO table_movies(title, path, tmdbId, languages, subtitles,`hearing_impaired`, radarrId, overview, poster, fanart, `audio_language`, sceneName, monitored, sortTitle) VALUES (?,?,?,(SELECT languages FROM table_movies WHERE tmdbId = ?), '[]',(SELECT `hearing_impaired` FROM table_movies WHERE tmdbId = ?), ?, ?, ?, ?, ?, ?, ?, ?)''', movies_to_add) db.commit() db.close() - added_movies = list(set(current_movies_radarr) - set(current_movies_db_list)) removed_movies = list(set(current_movies_db_list) - set(current_movies_radarr)) db = sqlite3.connect(os.path.join(args.config_dir, 'db', 'bazarr.db'), timeout=30) @@ -133,12 +115,11 @@ def update_movies(): db.commit() db.close() - db = sqlite3.connect(os.path.join(args.config_dir, 'db', 'bazarr.db'), timeout=30) - c = db.cursor() - for added_movie in added_movies: - added_path = c.execute('SELECT path FROM table_movies WHERE tmdbId = ?', (added_movie,)).fetchone() - store_subtitles_movie(path_replace_movie(added_path[0])) - db.close() + for added_movie in movies_to_add: + store_subtitles_movie(path_replace_movie(added_movie[1])) + + for updated_movie in movies_to_update: + store_subtitles_movie(path_replace_movie(updated_movie[1])) logging.debug('BAZARR All movies synced from Radarr into database.') diff --git a/bazarr/get_settings.py b/bazarr/get_settings.py index 20e079883..cc22d15af 100644 --- a/bazarr/get_settings.py +++ b/bazarr/get_settings.py @@ -41,10 +41,10 @@ def get_general_settings(): else: path_mappings = '[]' - if cfg.has_option('general', 'log_level'): - log_level = cfg.get('general', 'log_level') + if cfg.has_option('general', 'debug'): + debug = cfg.getboolean('general', 'debug') else: - log_level = 'INFO' + debug = False if cfg.has_option('general', 'branch'): branch = cfg.get('general', 'branch') @@ -156,7 +156,7 @@ def get_general_settings(): port = '6767' base_url = '/' path_mappings = '[]' - log_level = 'INFO' + debug = False branch = 'master' auto_update = True single_language = False @@ -179,11 +179,7 @@ def get_general_settings(): only_monitored = False adaptive_searching = False - return [ip, port, base_url, path_mappings, log_level, branch, auto_update, single_language, minimum_score, - use_scenename, use_postprocessing, postprocessing_cmd, use_sonarr, use_radarr, path_mappings_movie, - serie_default_enabled, serie_default_language, serie_default_hi, movie_default_enabled, - movie_default_language, movie_default_hi, page_size, minimum_score_movie, use_embedded_subs, only_monitored, - adaptive_searching] + return [ip, port, base_url, path_mappings, debug, branch, auto_update, single_language, minimum_score, use_scenename, use_postprocessing, postprocessing_cmd, use_sonarr, use_radarr, path_mappings_movie, serie_default_enabled, serie_default_language, serie_default_hi, movie_default_enabled,movie_default_language, movie_default_hi, page_size, minimum_score_movie, use_embedded_subs, only_monitored, adaptive_searching] def get_auth_settings(): @@ -458,7 +454,7 @@ ip = result[0] port = result[1] base_url = result[2] path_mappings = ast.literal_eval(result[3]) -log_level = result[4] +debug = result[4] branch = result[5] automatic = result[6] single_language = result[7] diff --git a/bazarr/get_subtitle.py b/bazarr/get_subtitle.py index a85eae490..88b0835b6 100644 --- a/bazarr/get_subtitle.py +++ b/bazarr/get_subtitle.py @@ -27,6 +27,7 @@ from utils import history_log, history_log_movie from notifier import send_notifications, send_notifications_movie from get_providers import get_providers, get_providers_auth from get_args import args +from subliminal.providers.legendastv import LegendasTVSubtitle # configure the cache @@ -165,7 +166,7 @@ def download_subtitle(path, language, hi, providers, providers_auth, sceneName, 'BAZARR ' + str(len(subtitles_list)) + " subtitles have been found for this file: " + path) if len(subtitles_list) > 0: try: - pdownload_result = False + download_result = False for subtitle in subtitles_list: download_result = p.download_subtitle(subtitle) if download_result == True: @@ -202,7 +203,7 @@ def download_subtitle(path, language, hi, providers, providers_auth, sceneName, downloaded_language = language_from_alpha3(result[0].language.alpha3) downloaded_language_code2 = alpha2_from_alpha3(result[0].language.alpha3) downloaded_language_code3 = result[0].language.alpha3 - downloaded_path = get_subtitle_path(path, language=language_set) + downloaded_path = get_subtitle_path(path, downloaded_language_code2) logging.debug('BAZARR Subtitles file saved to disk: ' + downloaded_path) if used_sceneName == True: message = downloaded_language + " subtitles downloaded from " + downloaded_provider + " with a score of " + unicode( @@ -302,6 +303,7 @@ def manual_search(path, language, hi, providers, providers_auth, sceneName, medi continue if used_sceneName: not_matched.remove('hash') + elif media_type == "series": matched = set(s.get_matches(video)) if hi == s.hearing_impaired: @@ -312,11 +314,17 @@ def manual_search(path, language, hi, providers, providers_auth, sceneName, medi continue if used_sceneName: not_matched.remove('hash') + + if type(s) is LegendasTVSubtitle: + # The pickle doesn't work very well with RAR (rarfile.RarFile) or ZIP (zipfile.ZipFile) + s.archive.content = None + subtitles_list.append( dict(score=round((compute_score(s, video, hearing_impaired=hi) / max_score * 100), 2), language=alpha2_from_alpha3(s.language.alpha3), hearing_impaired=str(s.hearing_impaired), provider=s.provider_name, subtitle=codecs.encode(pickle.dumps(s), "base64").decode(), url=s.page_link, matches=list(matched), dont_matches=list(not_matched))) + subtitles_dict = {} subtitles_dict = sorted(subtitles_list, key=lambda x: x['score'], reverse=True) logging.debug('BAZARR ' + str(len(subtitles_dict)) + " subtitles have been found for this file: " + path) @@ -381,7 +389,7 @@ def manual_download_subtitle(path, language, hi, subtitle, provider, providers_a downloaded_language = language_from_alpha3(result[0].language.alpha3) downloaded_language_code2 = alpha2_from_alpha3(result[0].language.alpha3) downloaded_language_code3 = result[0].language.alpha3 - downloaded_path = get_subtitle_path(path, language=lang_obj) + downloaded_path = get_subtitle_path(path, downloaded_language_code2) logging.debug('BAZARR Subtitles file saved to disk: ' + downloaded_path) message = downloaded_language + " subtitles downloaded from " + downloaded_provider + " with a score of " + unicode( score) + "% using manual search." diff --git a/bazarr/init.py b/bazarr/init.py index 8b11acb3a..86fe50a87 100644 --- a/bazarr/init.py +++ b/bazarr/init.py @@ -29,6 +29,11 @@ if not os.path.exists(os.path.join(args.config_dir, 'db')): if not os.path.exists(os.path.join(args.config_dir, 'log')): os.mkdir(os.path.join(args.config_dir, 'log')) logging.debug("BAZARR Created log folder") + +if not os.path.exists(os.path.join(config_dir, 'config', 'releases.txt')): + from check_update import check_releases + check_releases() + logging.debug("BAZARR Created releases file") config_file = os.path.normpath(os.path.join(args.config_dir, 'config', 'config.ini')) @@ -71,6 +76,13 @@ if cfg.has_section('auth'): cfg.remove_option('auth', 'enabled') with open(config_file, 'w+') as configfile: cfg.write(configfile) + +if cfg.has_section('general'): + if cfg.has_option('general', 'log_level'): + cfg.remove_option('general', 'log_level') + cfg.set('general', 'debug', 'False') + with open(config_file, 'w+') as configfile: + cfg.write(configfile) if not os.path.exists(os.path.normpath(os.path.join(args.config_dir, 'config', 'users.json'))): cork = Cork(os.path.normpath(os.path.join(args.config_dir, 'config')), initialize=True) diff --git a/bazarr/list_subtitles.py b/bazarr/list_subtitles.py index 719982409..fe4e6819a 100644 --- a/bazarr/list_subtitles.py +++ b/bazarr/list_subtitles.py @@ -60,13 +60,13 @@ def store_subtitles(file): encoding = UnicodeDammit(text) try: text = text.decode(encoding.original_encoding) + detected_language = langdetect.detect(text) except Exception as e: logging.exception( 'BAZARR Error trying to detect character encoding for this subtitles file: ' + path_replace( os.path.join(os.path.dirname(file), subtitle)) + ' You should try to delete this subtitles file manually and ask Bazarr to download it again.') else: - detected_language = langdetect.detect(text) if len(detected_language) > 0: actual_subtitles.append([str(detected_language), path_replace_reverse( os.path.join(os.path.dirname(file), subtitle))]) @@ -119,13 +119,13 @@ def store_subtitles_movie(file): encoding = UnicodeDammit(text) try: text = text.decode(encoding.original_encoding) + detected_language = langdetect.detect(text) except Exception as e: logging.exception( 'BAZARR Error trying to detect character encoding for this subtitles file: ' + path_replace_movie( os.path.join(os.path.dirname(file), subtitle)) + ' You should try to delete this subtitles file manually and ask Bazarr to download it again.') else: - detected_language = langdetect.detect(text) if len(detected_language) > 0: actual_subtitles.append([str(detected_language), path_replace_reverse_movie( os.path.join(os.path.dirname(file), subtitle))]) diff --git a/bazarr/logger.py b/bazarr/logger.py new file mode 100644 index 000000000..daf436356 --- /dev/null +++ b/bazarr/logger.py @@ -0,0 +1,145 @@ +import os +import logging +import re + +from logging.handlers import TimedRotatingFileHandler +from get_argv import config_dir + +logger = logging.getLogger() + + +class OneLineExceptionFormatter(logging.Formatter): + def formatException(self, exc_info): + """ + Format an exception so that it prints on a single line. + """ + result = super(OneLineExceptionFormatter, self).formatException(exc_info) + return repr(result) # or format into one line however you want to + + def format(self, record): + s = super(OneLineExceptionFormatter, self).format(record) + if record.exc_text: + s = s.replace('\n', '') + '|' + return s + + +class NoExceptionFormatter(logging.Formatter): + def format(self, record): + record.exc_text = '' # ensure formatException gets called + return super(NoExceptionFormatter, self).format(record) + + def formatException(self, record): + return '' + + +def configure_logging(debug=False): + if not debug: + log_level = "INFO" + else: + log_level = "DEBUG" + + logger.handlers = [] + + logger.setLevel(log_level) + + # Console logging + ch = logging.StreamHandler() + cf = NoExceptionFormatter('%(asctime)-15s - %(name)-32s (%(thread)x) : %(levelname)s (%(module)s:%(lineno)d) ' + '- %(message)s') + ch.setFormatter(cf) + + ch.setLevel(log_level) + # ch.addFilter(MyFilter()) + logger.addHandler(ch) + + # File Logging + global fh + fh = TimedRotatingFileHandler(os.path.join(config_dir, 'log/bazarr.log'), when="midnight", interval=1, + backupCount=7) + f = OneLineExceptionFormatter('%(asctime)s|%(levelname)-8s|%(name)-32s|%(message)s|', + '%d/%m/%Y %H:%M:%S') + fh.setFormatter(f) + fh.addFilter(BlacklistFilter()) + fh.addFilter(PublicIPFilter()) + + if debug: + logging.getLogger("apscheduler").setLevel(logging.DEBUG) + logging.getLogger("subliminal").setLevel(logging.DEBUG) + logging.getLogger("git").setLevel(logging.DEBUG) + logging.getLogger("apprise").setLevel(logging.DEBUG) + else: + logging.getLogger("apscheduler").setLevel(logging.WARNING) + logging.getLogger("subliminal").setLevel(logging.CRITICAL) + + logging.getLogger("enzyme").setLevel(logging.CRITICAL) + logging.getLogger("guessit").setLevel(logging.WARNING) + logging.getLogger("rebulk").setLevel(logging.WARNING) + logging.getLogger("stevedore.extension").setLevel(logging.CRITICAL) + fh.setLevel(log_level) + logger.addHandler(fh) + + +class MyFilter(logging.Filter): + def __init__(self): + pass + + def filter(self, record): + if record.name != 'root': + return False + return True + + +class BlacklistFilter(logging.Filter): + """ + Log filter for blacklisted tokens and passwords + """ + def __init__(self): + pass + + def filter(self, record): + try: + apikeys = re.findall(r'apikey(?:=|%3D)([a-zA-Z0-9]+)', record.msg) + for apikey in apikeys: + record.msg = record.msg.replace(apikey, 8 * '*' + apikey[-2:]) + + args = [] + for arg in record.args: + apikeys = re.findall(r'apikey(?:=|%3D)([a-zA-Z0-9]+)', arg) if isinstance(arg, basestring) else [] + for apikey in apikeys: + arg = arg.replace(apikey, 8 * '*' + apikey[-2:]) + args.append(arg) + record.args = tuple(args) + except: + pass + return True + + +class PublicIPFilter(logging.Filter): + """ + Log filter for public IP addresses + """ + def __init__(self): + pass + + def filter(self, record): + try: + # Currently only checking for ipv4 addresses + ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}(?!\d*-[a-z0-9]{6})', record.msg) + for ip in ipv4: + record.msg = record.msg.replace(ip, ip.partition('.')[0] + '.***.***.***') + + args = [] + for arg in record.args: + ipv4 = re.findall(r'[0-9]+(?:\.[0-9]+){3}(?!\d*-[a-z0-9]{6})', arg) if isinstance(arg, basestring) else [] + for ip in ipv4: + arg = arg.replace(ip, ip.partition('.')[0] + '.***.***.***') + args.append(arg) + record.args = tuple(args) + except: + pass + + return True + + +def empty_log(): + fh.doRollover() diff --git a/bazarr/main.py b/bazarr/main.py index 6e42ce00c..59ac572ae 100644 --- a/bazarr/main.py +++ b/bazarr/main.py @@ -13,19 +13,15 @@ from init import * from update_db import * from notifier import update_notifier from get_settings import get_general_settings, get_proxy_settings -from logging.handlers import TimedRotatingFileHandler +from logger import configure_logging, empty_log reload(sys) sys.setdefaultencoding('utf8') gc.enable() update_notifier() -bazarr_version = '0.6.7' - -log_level = get_general_settings()[4] -if log_level is None: - log_level = "INFO" - +bazarr_version = '0.6.9' +configure_logging(get_general_settings()[4]) class OneLineExceptionFormatter(logging.Formatter): def formatException(self, exc_info): @@ -41,42 +37,7 @@ class OneLineExceptionFormatter(logging.Formatter): s = s.replace('\n', '') + '|' return s - -fh = None - - -def configure_logging(console_debug=False): - global fh - fh = TimedRotatingFileHandler(os.path.join(args.config_dir, 'log', 'bazarr.log'), when="midnight", interval=1, - backupCount=7) - f = OneLineExceptionFormatter('%(asctime)s|%(levelname)s|%(message)s|', - '%d/%m/%Y %H:%M:%S') - fh.setFormatter(f) - logging.getLogger("enzyme").setLevel(logging.CRITICAL) - logging.getLogger("apscheduler").setLevel(logging.WARNING) - logging.getLogger("subliminal").setLevel(logging.CRITICAL) - logging.getLogger("subliminal_patch").setLevel(logging.CRITICAL) - logging.getLogger("subzero").setLevel(logging.WARNING) - logging.getLogger("guessit").setLevel(logging.WARNING) - logging.getLogger("rebulk").setLevel(logging.WARNING) - logging.getLogger("urllib3").setLevel(logging.WARNING) - logging.getLogger("stevedore.extension").setLevel(logging.CRITICAL) - root = logging.getLogger() - root.setLevel(log_level) - root.addHandler(fh) - - if console_debug: - logging.getLogger("subliminal").setLevel(logging.DEBUG) - logging.getLogger("subliminal_patch").setLevel(logging.DEBUG) - logging.getLogger("subzero").setLevel(logging.DEBUG) - sh = logging.StreamHandler(sys.stdout) - cf = logging.Formatter('%(asctime)-15s - %(name)-32s (%(thread)x) : %(levelname)s (%(module)s:%(lineno)d) ' - '- %(message)s') - sh.setFormatter(cf) - root.addHandler(sh) - - -configure_logging(console_debug=args.debug) +configure_logging(get_general_settings()[4]) import requests @@ -94,8 +55,11 @@ from bottle import route, run, template, static_file, request, redirect, respons import bottle bottle.TEMPLATE_PATH.insert(0, os.path.join(os.path.dirname(__file__), '../views/')) -bottle.debug(True) -bottle.TEMPLATES.clear() +if "PYCHARM_HOSTED" in os.environ: + bottle.debug(True) + bottle.TEMPLATES.clear() +else: + bottle.ERROR_PAGE_TEMPLATE = bottle.ERROR_PAGE_TEMPLATE.replace('if DEBUG and', 'if') from cherrypy.wsgiserver import CherryPyWSGIServer @@ -253,7 +217,7 @@ def restart(): except Exception as e: logging.error('BAZARR Cannot create bazarr.restart file.') else: - print 'Bazarr is being restarted...' + # print 'Bazarr is being restarted...' logging.info('Bazarr is being restarted...') restart_file.write('') restart_file.close() @@ -480,7 +444,7 @@ def emptylog(): authorize() ref = request.environ['HTTP_REFERER'] - fh.doRollover() + empty_log() logging.info('BAZARR Log file emptied') redirect(ref) @@ -536,6 +500,7 @@ def image_proxy_movies(url): @route(base_url) +@route(base_url.rstrip('/')) @custom_auth_basic(check_credentials) def redirect_root(): authorize() @@ -737,9 +702,7 @@ def episodes(no): (str(no),)).fetchone() tvdbid = series_details[5] - episodes = c.execute( - "SELECT title, path_substitution(path), season, episode, subtitles, sonarrSeriesId, missing_subtitles, sonarrEpisodeId, scene_name, monitored FROM table_episodes WHERE sonarrSeriesId LIKE ? ORDER BY episode ASC", - (str(no),)).fetchall() + episodes = c.execute("SELECT title, path_substitution(path), season, episode, subtitles, sonarrSeriesId, missing_subtitles, sonarrEpisodeId, scene_name, monitored, failedAttempts FROM table_episodes WHERE sonarrSeriesId LIKE ? ORDER BY episode ASC", (str(no),)).fetchall() number = len(episodes) languages = c.execute("SELECT code2, name FROM table_settings_languages WHERE enabled = 1").fetchall() c.close() @@ -773,9 +736,7 @@ def movies(): offset = (int(page) - 1) * page_size max_page = int(math.ceil(missing_count / (page_size + 0.0))) - c.execute( - "SELECT tmdbId, title, path_substitution(path), languages, hearing_impaired, radarrId, poster, audio_language, monitored FROM table_movies ORDER BY title ASC LIMIT ? OFFSET ?", - (page_size, offset,)) + c.execute("SELECT tmdbId, title, path_substitution(path), languages, hearing_impaired, radarrId, poster, audio_language, monitored, sceneName FROM table_movies ORDER BY sortTitle ASC LIMIT ? OFFSET ?", (page_size, offset,)) data = c.fetchall() c.execute("SELECT code2, name FROM table_settings_languages WHERE enabled = 1") languages = c.fetchall() @@ -890,9 +851,7 @@ def movie(no): c = conn.cursor() movies_details = [] - movies_details = c.execute( - "SELECT title, overview, poster, fanart, hearing_impaired, tmdbid, audio_language, languages, path_substitution(path), subtitles, radarrId, missing_subtitles, sceneName, monitored FROM table_movies WHERE radarrId LIKE ?", - (str(no),)).fetchone() + movies_details = c.execute("SELECT title, overview, poster, fanart, hearing_impaired, tmdbid, audio_language, languages, path_substitution(path), subtitles, radarrId, missing_subtitles, sceneName, monitored, failedAttempts FROM table_movies WHERE radarrId LIKE ?", (str(no),)).fetchone() tmdbid = movies_details[5] languages = c.execute("SELECT code2, name FROM table_settings_languages WHERE enabled = 1").fetchall() @@ -1070,9 +1029,7 @@ def wantedseries(): offset = (int(page) - 1) * page_size max_page = int(math.ceil(missing_count / (page_size + 0.0))) - c.execute( - "SELECT table_shows.title, table_episodes.season || 'x' || table_episodes.episode, table_episodes.title, table_episodes.missing_subtitles, table_episodes.sonarrSeriesId, path_substitution(table_episodes.path), table_shows.hearing_impaired, table_episodes.sonarrEpisodeId, table_episodes.scene_name FROM table_episodes INNER JOIN table_shows on table_shows.sonarrSeriesId = table_episodes.sonarrSeriesId WHERE table_episodes.missing_subtitles != '[]'" + monitored_only_query_string + " ORDER BY table_episodes._rowid_ DESC LIMIT ? OFFSET ?", - (page_size, offset,)) + c.execute("SELECT table_shows.title, table_episodes.season || 'x' || table_episodes.episode, table_episodes.title, table_episodes.missing_subtitles, table_episodes.sonarrSeriesId, path_substitution(table_episodes.path), table_shows.hearing_impaired, table_episodes.sonarrEpisodeId, table_episodes.scene_name, table_episodes.failedAttempts FROM table_episodes INNER JOIN table_shows on table_shows.sonarrSeriesId = table_episodes.sonarrSeriesId WHERE table_episodes.missing_subtitles != '[]'" + monitored_only_query_string + " ORDER BY table_episodes._rowid_ DESC LIMIT ? OFFSET ?", (page_size, offset,)) data = c.fetchall() c.close() return template('wantedseries', __file__=__file__, bazarr_version=bazarr_version, rows=data, @@ -1103,9 +1060,7 @@ def wantedmovies(): offset = (int(page) - 1) * page_size max_page = int(math.ceil(missing_count / (page_size + 0.0))) - c.execute( - "SELECT title, missing_subtitles, radarrId, path_substitution(path), hearing_impaired, sceneName FROM table_movies WHERE missing_subtitles != '[]'" + monitored_only_query_string + " ORDER BY _rowid_ DESC LIMIT ? OFFSET ?", - (page_size, offset,)) + c.execute("SELECT title, missing_subtitles, radarrId, path_substitution(path), hearing_impaired, sceneName, failedAttempts FROM table_movies WHERE missing_subtitles != '[]'" + monitored_only_query_string + " ORDER BY _rowid_ DESC LIMIT ? OFFSET ?", (page_size, offset,)) data = c.fetchall() c.close() return template('wantedmovies', __file__=__file__, bazarr_version=bazarr_version, rows=data, @@ -1167,7 +1122,11 @@ def save_settings(): settings_general_baseurl = request.forms.get('settings_general_baseurl') if not settings_general_baseurl.endswith('/'): settings_general_baseurl += '/' - settings_general_loglevel = request.forms.get('settings_general_loglevel') + settings_general_debug = request.forms.get('settings_general_debug') + if settings_general_debug is None: + settings_general_debug = 'False' + else: + settings_general_debug = 'True' settings_general_sourcepath = request.forms.getall('settings_general_sourcepath') settings_general_destpath = request.forms.getall('settings_general_destpath') settings_general_pathmapping = [] @@ -1230,14 +1189,8 @@ def save_settings(): settings_general = get_general_settings() - before = ( - unicode(settings_general[0]), int(settings_general[1]), unicode(settings_general[2]), unicode(settings_general[4]), - unicode(settings_general[3]), unicode(settings_general[12]), unicode(settings_general[13]), - unicode(settings_general[14])) - after = (unicode(settings_general_ip), int(settings_general_port), unicode(settings_general_baseurl), - unicode(settings_general_loglevel), unicode(settings_general_pathmapping), - unicode(settings_general_use_sonarr), unicode(settings_general_use_radarr), - unicode(settings_general_pathmapping_movie)) + before = (unicode(settings_general[0]), int(settings_general[1]), unicode(settings_general[2]), unicode(settings_general[3]), unicode(settings_general[12]), unicode(settings_general[13]), unicode(settings_general[14])) + after = (unicode(settings_general_ip), int(settings_general_port), unicode(settings_general_baseurl), unicode(settings_general_pathmapping), unicode(settings_general_use_sonarr), unicode(settings_general_use_radarr), unicode(settings_general_pathmapping_movie)) from six import text_type cfg = ConfigParser() @@ -1249,7 +1202,7 @@ def save_settings(): cfg.set('general', 'port', text_type(settings_general_port)) cfg.set('general', 'base_url', text_type(settings_general_baseurl)) cfg.set('general', 'path_mappings', text_type(settings_general_pathmapping)) - cfg.set('general', 'log_level', text_type(settings_general_loglevel)) + cfg.set('general', 'debug', text_type(settings_general_debug)) cfg.set('general', 'branch', text_type(settings_general_branch)) cfg.set('general', 'auto_update', text_type(settings_general_automatic)) cfg.set('general', 'single_language', text_type(settings_general_single_language)) @@ -1438,6 +1391,8 @@ def save_settings(): with open(config_file, 'wb') as f: cfg.write(f) + configure_logging(get_general_settings()[4]) + notifiers = c.execute("SELECT * FROM table_settings_notifier ORDER BY name").fetchall() for notifier in notifiers: enabled = request.forms.get('settings_notifier_' + notifier[0] + '_enabled') @@ -1578,25 +1533,32 @@ def system(): page_size = int(get_general_settings()[21]) max_page = int(math.ceil(row_count / (page_size + 0.0))) - releases = [] - url_releases = 'https://api.github.com/repos/morpheus65535/Bazarr/releases' - try: - r = requests.get(url_releases, timeout=15) - r.raise_for_status() - except requests.exceptions.HTTPError as errh: - logging.exception("BAZARR Error trying to get releases from Github. Http error.") - except requests.exceptions.ConnectionError as errc: - logging.exception("BAZARR Error trying to get releases from Github. Connection Error.") - except requests.exceptions.Timeout as errt: - logging.exception("BAZARR Error trying to get releases from Github. Timeout Error.") - except requests.exceptions.RequestException as err: - logging.exception("BAZARR Error trying to get releases from Github.") - else: - for release in r.json(): - releases.append([release['name'], release['body']]) + with open(os.path.join(config_dir, 'config', 'releases.txt'), 'r') as f: + releases = ast.literal_eval(f.read()) - return template('system', __file__=__file__, bazarr_version=bazarr_version, base_url=base_url, task_list=task_list, - row_count=row_count, max_page=max_page, page_size=page_size, releases=releases, current_port=port) + import platform + url_sonarr = get_sonarr_settings()[6] + apikey_sonarr = get_sonarr_settings()[4] + sv = url_sonarr + "/api/system/status?apikey=" + apikey_sonarr + try: + sonarr_version = requests.get(sv, timeout=15, verify=False) + except: + sonarr_version = '' + + url_radarr = get_radarr_settings()[6] + apikey_radarr = get_radarr_settings()[4] + sv = url_radarr + "/api/system/status?apikey=" + apikey_radarr + try: + radarr_version = requests.get(sv, timeout=15, verify=False) + except: + radarr_version = '' + + return template('system', __file__=__file__, bazarr_version=bazarr_version, + sonarr_version=sonarr_version.json()['version'], radarr_version=radarr_version.json()['version'], + operating_system=platform.platform(), python_version=platform.python_version(), + config_dir=config_dir, bazarr_dir=os.path.normcase(os.getcwd()), + base_url=base_url, task_list=task_list, row_count=row_count, max_page=max_page, page_size=page_size, + releases=releases, current_port=port) @route(base_url + 'logs/') @@ -1886,7 +1848,7 @@ warnings.simplefilter("ignore", DeprecationWarning) server = CherryPyWSGIServer((str(ip), int(port)), app) try: logging.info('BAZARR is started and waiting for request on http://' + str(ip) + ':' + str(port) + str(base_url)) - print 'Bazarr is started and waiting for request on http://' + str(ip) + ':' + str(port) + str(base_url) + # print 'Bazarr is started and waiting for request on http://' + str(ip) + ':' + str(port) + str(base_url) server.start() except KeyboardInterrupt: shutdown() diff --git a/bazarr/scheduler.py b/bazarr/scheduler.py index 356a1d66c..f64a26e32 100644 --- a/bazarr/scheduler.py +++ b/bazarr/scheduler.py @@ -10,6 +10,8 @@ from get_args import args if not args.no_update: from check_update import check_and_apply_update +else: + from check_update import check_releases from apscheduler.schedulers.background import BackgroundScheduler from apscheduler.triggers.interval import IntervalTrigger @@ -73,8 +75,11 @@ if not args.no_update: scheduler.add_job(check_and_apply_update, IntervalTrigger(hours=6), max_instances=1, coalesce=True, misfire_grace_time=15, id='update_bazarr', name='Update bazarr from source on Github') else: - scheduler.add_job(check_and_apply_update, CronTrigger(year='2100'), hour=4, id='update_bazarr', - name='Update bazarr from source on Github') + scheduler.add_job(check_and_apply_update, CronTrigger(year='2100'), hour=4, id='update_bazarr', name='Update bazarr from source on Github') + scheduler.add_job(check_releases, IntervalTrigger(hours=6), max_instances=1, coalesce=True, misfire_grace_time=15, id='update_release', name='Update release info') +else: + scheduler.add_job(check_releases, IntervalTrigger(hours=6), max_instances=1, coalesce=True, misfire_grace_time=15, + id='update_release', name='Update release info') if integration[12] is True: scheduler.add_job(update_series, IntervalTrigger(minutes=1), max_instances=1, coalesce=True, misfire_grace_time=15, diff --git a/bazarr/update_db.py b/bazarr/update_db.py index 935f83eb6..029db84e7 100644 --- a/bazarr/update_db.py +++ b/bazarr/update_db.py @@ -36,6 +36,11 @@ if os.path.exists(os.path.join(args.config_dir, 'db', 'bazarr.db')): except: pass + try: + c.execute('alter table table_movies add column "sortTitle" "text"') + except: + pass + try: rows = c.execute('SELECT name FROM table_settings_notifier WHERE name = "Kodi/XBMC"').fetchall() if len(rows) == 0: diff --git a/libs/waitress/__init__.py b/libs/waitress/__init__.py deleted file mode 100644 index 775fe3a50..000000000 --- a/libs/waitress/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -from waitress.server import create_server -import logging - -def serve(app, **kw): - _server = kw.pop('_server', create_server) # test shim - _quiet = kw.pop('_quiet', False) # test shim - _profile = kw.pop('_profile', False) # test shim - if not _quiet: # pragma: no cover - # idempotent if logging has already been set up - logging.basicConfig() - server = _server(app, **kw) - if not _quiet: # pragma: no cover - server.print_listen('Serving on http://{}:{}') - if _profile: # pragma: no cover - profile('server.run()', globals(), locals(), (), False) - else: - server.run() - -def serve_paste(app, global_conf, **kw): - serve(app, **kw) - return 0 - -def profile(cmd, globals, locals, sort_order, callers): # pragma: no cover - # runs a command under the profiler and print profiling output at shutdown - import os - import profile - import pstats - import tempfile - fd, fn = tempfile.mkstemp() - try: - profile.runctx(cmd, globals, locals, fn) - stats = pstats.Stats(fn) - stats.strip_dirs() - # calls,time,cumulative and cumulative,calls,time are useful - stats.sort_stats(*(sort_order or ('cumulative', 'calls', 'time'))) - if callers: - stats.print_callers(.3) - else: - stats.print_stats(.3) - finally: - os.remove(fn) diff --git a/libs/waitress/__main__.py b/libs/waitress/__main__.py deleted file mode 100644 index e484f40a7..000000000 --- a/libs/waitress/__main__.py +++ /dev/null @@ -1,2 +0,0 @@ -from waitress.runner import run # pragma nocover -run() # pragma nocover diff --git a/libs/waitress/adjustments.py b/libs/waitress/adjustments.py deleted file mode 100644 index 1a5662198..000000000 --- a/libs/waitress/adjustments.py +++ /dev/null @@ -1,340 +0,0 @@ -############################################################################## -# -# Copyright (c) 2002 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -"""Adjustments are tunable parameters. -""" -import getopt -import socket - -from waitress.compat import ( - PY2, - WIN, - string_types, - HAS_IPV6, - ) - -truthy = frozenset(('t', 'true', 'y', 'yes', 'on', '1')) - -def asbool(s): - """ Return the boolean value ``True`` if the case-lowered value of string - input ``s`` is any of ``t``, ``true``, ``y``, ``on``, or ``1``, otherwise - return the boolean value ``False``. If ``s`` is the value ``None``, - return ``False``. If ``s`` is already one of the boolean values ``True`` - or ``False``, return it.""" - if s is None: - return False - if isinstance(s, bool): - return s - s = str(s).strip() - return s.lower() in truthy - -def asoctal(s): - """Convert the given octal string to an actual number.""" - return int(s, 8) - -def aslist_cronly(value): - if isinstance(value, string_types): - value = filter(None, [x.strip() for x in value.splitlines()]) - return list(value) - -def aslist(value): - """ Return a list of strings, separating the input based on newlines - and, if flatten=True (the default), also split on spaces within - each line.""" - values = aslist_cronly(value) - result = [] - for value in values: - subvalues = value.split() - result.extend(subvalues) - return result - -def slash_fixed_str(s): - s = s.strip() - if s: - # always have a leading slash, replace any number of leading slashes - # with a single slash, and strip any trailing slashes - s = '/' + s.lstrip('/').rstrip('/') - return s - -class _str_marker(str): - pass - -class _int_marker(int): - pass - -class Adjustments(object): - """This class contains tunable parameters. - """ - - _params = ( - ('host', str), - ('port', int), - ('ipv4', asbool), - ('ipv6', asbool), - ('listen', aslist), - ('threads', int), - ('trusted_proxy', str), - ('url_scheme', str), - ('url_prefix', slash_fixed_str), - ('backlog', int), - ('recv_bytes', int), - ('send_bytes', int), - ('outbuf_overflow', int), - ('inbuf_overflow', int), - ('connection_limit', int), - ('cleanup_interval', int), - ('channel_timeout', int), - ('log_socket_errors', asbool), - ('max_request_header_size', int), - ('max_request_body_size', int), - ('expose_tracebacks', asbool), - ('ident', str), - ('asyncore_loop_timeout', int), - ('asyncore_use_poll', asbool), - ('unix_socket', str), - ('unix_socket_perms', asoctal), - ) - - _param_map = dict(_params) - - # hostname or IP address to listen on - host = _str_marker('0.0.0.0') - - # TCP port to listen on - port = _int_marker(8080) - - listen = ['{}:{}'.format(host, port)] - - # mumber of threads available for tasks - threads = 4 - - # Host allowed to overrid ``wsgi.url_scheme`` via header - trusted_proxy = None - - # default ``wsgi.url_scheme`` value - url_scheme = 'http' - - # default ``SCRIPT_NAME`` value, also helps reset ``PATH_INFO`` - # when nonempty - url_prefix = '' - - # server identity (sent in Server: header) - ident = 'waitress' - - # backlog is the value waitress passes to pass to socket.listen() This is - # the maximum number of incoming TCP connections that will wait in an OS - # queue for an available channel. From listen(1): "If a connection - # request arrives when the queue is full, the client may receive an error - # with an indication of ECONNREFUSED or, if the underlying protocol - # supports retransmission, the request may be ignored so that a later - # reattempt at connection succeeds." - backlog = 1024 - - # recv_bytes is the argument to pass to socket.recv(). - recv_bytes = 8192 - - # send_bytes is the number of bytes to send to socket.send(). Multiples - # of 9000 should avoid partly-filled packets, but don't set this larger - # than the TCP write buffer size. In Linux, /proc/sys/net/ipv4/tcp_wmem - # controls the minimum, default, and maximum sizes of TCP write buffers. - send_bytes = 18000 - - # A tempfile should be created if the pending output is larger than - # outbuf_overflow, which is measured in bytes. The default is 1MB. This - # is conservative. - outbuf_overflow = 1048576 - - # A tempfile should be created if the pending input is larger than - # inbuf_overflow, which is measured in bytes. The default is 512K. This - # is conservative. - inbuf_overflow = 524288 - - # Stop creating new channels if too many are already active (integer). - # Each channel consumes at least one file descriptor, and, depending on - # the input and output body sizes, potentially up to three. The default - # is conservative, but you may need to increase the number of file - # descriptors available to the Waitress process on most platforms in - # order to safely change it (see ``ulimit -a`` "open files" setting). - # Note that this doesn't control the maximum number of TCP connections - # that can be waiting for processing; the ``backlog`` argument controls - # that. - connection_limit = 100 - - # Minimum seconds between cleaning up inactive channels. - cleanup_interval = 30 - - # Maximum seconds to leave an inactive connection open. - channel_timeout = 120 - - # Boolean: turn off to not log premature client disconnects. - log_socket_errors = True - - # maximum number of bytes of all request headers combined (256K default) - max_request_header_size = 262144 - - # maximum number of bytes in request body (1GB default) - max_request_body_size = 1073741824 - - # expose tracebacks of uncaught exceptions - expose_tracebacks = False - - # Path to a Unix domain socket to use. - unix_socket = None - - # Path to a Unix domain socket to use. - unix_socket_perms = 0o600 - - # The socket options to set on receiving a connection. It is a list of - # (level, optname, value) tuples. TCP_NODELAY disables the Nagle - # algorithm for writes (Waitress already buffers its writes). - socket_options = [ - (socket.SOL_TCP, socket.TCP_NODELAY, 1), - ] - - # The asyncore.loop timeout value - asyncore_loop_timeout = 1 - - # The asyncore.loop flag to use poll() instead of the default select(). - asyncore_use_poll = False - - # Enable IPv4 by default - ipv4 = True - - # Enable IPv6 by default - ipv6 = True - - def __init__(self, **kw): - - if 'listen' in kw and ('host' in kw or 'port' in kw): - raise ValueError('host and or port may not be set if listen is set.') - - for k, v in kw.items(): - if k not in self._param_map: - raise ValueError('Unknown adjustment %r' % k) - setattr(self, k, self._param_map[k](v)) - - if (not isinstance(self.host, _str_marker) or - not isinstance(self.port, _int_marker)): - self.listen = ['{}:{}'.format(self.host, self.port)] - - enabled_families = socket.AF_UNSPEC - - if not self.ipv4 and not HAS_IPV6: # pragma: no cover - raise ValueError( - 'IPv4 is disabled but IPv6 is not available. Cowardly refusing to start.' - ) - - if self.ipv4 and not self.ipv6: - enabled_families = socket.AF_INET - - if not self.ipv4 and self.ipv6 and HAS_IPV6: - enabled_families = socket.AF_INET6 - - wanted_sockets = [] - hp_pairs = [] - for i in self.listen: - if ':' in i: - (host, port) = i.rsplit(":", 1) - - # IPv6 we need to make sure that we didn't split on the address - if ']' in port: # pragma: nocover - (host, port) = (i, str(self.port)) - else: - (host, port) = (i, str(self.port)) - - if WIN and PY2: # pragma: no cover - try: - # Try turning the port into an integer - port = int(port) - except: - raise ValueError( - 'Windows does not support service names instead of port numbers' - ) - - try: - if '[' in host and ']' in host: # pragma: nocover - host = host.strip('[').rstrip(']') - - if host == '*': - host = None - - for s in socket.getaddrinfo( - host, - port, - enabled_families, - socket.SOCK_STREAM, - socket.IPPROTO_TCP, - socket.AI_PASSIVE - ): - (family, socktype, proto, _, sockaddr) = s - - # It seems that getaddrinfo() may sometimes happily return - # the same result multiple times, this of course makes - # bind() very unhappy... - # - # Split on %, and drop the zone-index from the host in the - # sockaddr. Works around a bug in OS X whereby - # getaddrinfo() returns the same link-local interface with - # two different zone-indices (which makes no sense what so - # ever...) yet treats them equally when we attempt to bind(). - if ( - sockaddr[1] == 0 or - (sockaddr[0].split('%', 1)[0], sockaddr[1]) not in hp_pairs - ): - wanted_sockets.append((family, socktype, proto, sockaddr)) - hp_pairs.append((sockaddr[0].split('%', 1)[0], sockaddr[1])) - except: - raise ValueError('Invalid host/port specified.') - - self.listen = wanted_sockets - - @classmethod - def parse_args(cls, argv): - """Pre-parse command line arguments for input into __init__. Note that - this does not cast values into adjustment types, it just creates a - dictionary suitable for passing into __init__, where __init__ does the - casting. - """ - long_opts = ['help', 'call'] - for opt, cast in cls._params: - opt = opt.replace('_', '-') - if cast is asbool: - long_opts.append(opt) - long_opts.append('no-' + opt) - else: - long_opts.append(opt + '=') - - kw = { - 'help': False, - 'call': False, - } - - opts, args = getopt.getopt(argv, '', long_opts) - for opt, value in opts: - param = opt.lstrip('-').replace('-', '_') - - if param == 'listen': - kw['listen'] = '{} {}'.format(kw.get('listen', ''), value) - continue - - if param.startswith('no_'): - param = param[3:] - kw[param] = 'false' - elif param in ('help', 'call'): - kw[param] = True - elif cls._param_map[param] is asbool: - kw[param] = 'true' - else: - kw[param] = value - - return kw, args diff --git a/libs/waitress/buffers.py b/libs/waitress/buffers.py deleted file mode 100644 index cacc09474..000000000 --- a/libs/waitress/buffers.py +++ /dev/null @@ -1,298 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001-2004 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -"""Buffers -""" -from io import BytesIO - -# copy_bytes controls the size of temp. strings for shuffling data around. -COPY_BYTES = 1 << 18 # 256K - -# The maximum number of bytes to buffer in a simple string. -STRBUF_LIMIT = 8192 - -class FileBasedBuffer(object): - - remain = 0 - - def __init__(self, file, from_buffer=None): - self.file = file - if from_buffer is not None: - from_file = from_buffer.getfile() - read_pos = from_file.tell() - from_file.seek(0) - while True: - data = from_file.read(COPY_BYTES) - if not data: - break - file.write(data) - self.remain = int(file.tell() - read_pos) - from_file.seek(read_pos) - file.seek(read_pos) - - def __len__(self): - return self.remain - - def __nonzero__(self): - return True - - __bool__ = __nonzero__ # py3 - - def append(self, s): - file = self.file - read_pos = file.tell() - file.seek(0, 2) - file.write(s) - file.seek(read_pos) - self.remain = self.remain + len(s) - - def get(self, numbytes=-1, skip=False): - file = self.file - if not skip: - read_pos = file.tell() - if numbytes < 0: - # Read all - res = file.read() - else: - res = file.read(numbytes) - if skip: - self.remain -= len(res) - else: - file.seek(read_pos) - return res - - def skip(self, numbytes, allow_prune=0): - if self.remain < numbytes: - raise ValueError("Can't skip %d bytes in buffer of %d bytes" % ( - numbytes, self.remain) - ) - self.file.seek(numbytes, 1) - self.remain = self.remain - numbytes - - def newfile(self): - raise NotImplementedError() - - def prune(self): - file = self.file - if self.remain == 0: - read_pos = file.tell() - file.seek(0, 2) - sz = file.tell() - file.seek(read_pos) - if sz == 0: - # Nothing to prune. - return - nf = self.newfile() - while True: - data = file.read(COPY_BYTES) - if not data: - break - nf.write(data) - self.file = nf - - def getfile(self): - return self.file - - def close(self): - if hasattr(self.file, 'close'): - self.file.close() - self.remain = 0 - -class TempfileBasedBuffer(FileBasedBuffer): - - def __init__(self, from_buffer=None): - FileBasedBuffer.__init__(self, self.newfile(), from_buffer) - - def newfile(self): - from tempfile import TemporaryFile - return TemporaryFile('w+b') - -class BytesIOBasedBuffer(FileBasedBuffer): - - def __init__(self, from_buffer=None): - if from_buffer is not None: - FileBasedBuffer.__init__(self, BytesIO(), from_buffer) - else: - # Shortcut. :-) - self.file = BytesIO() - - def newfile(self): - return BytesIO() - -class ReadOnlyFileBasedBuffer(FileBasedBuffer): - # used as wsgi.file_wrapper - - def __init__(self, file, block_size=32768): - self.file = file - self.block_size = block_size # for __iter__ - - def prepare(self, size=None): - if hasattr(self.file, 'seek') and hasattr(self.file, 'tell'): - start_pos = self.file.tell() - self.file.seek(0, 2) - end_pos = self.file.tell() - self.file.seek(start_pos) - fsize = end_pos - start_pos - if size is None: - self.remain = fsize - else: - self.remain = min(fsize, size) - return self.remain - - def get(self, numbytes=-1, skip=False): - # never read more than self.remain (it can be user-specified) - if numbytes == -1 or numbytes > self.remain: - numbytes = self.remain - file = self.file - if not skip: - read_pos = file.tell() - res = file.read(numbytes) - if skip: - self.remain -= len(res) - else: - file.seek(read_pos) - return res - - def __iter__(self): # called by task if self.filelike has no seek/tell - return self - - def next(self): - val = self.file.read(self.block_size) - if not val: - raise StopIteration - return val - - __next__ = next # py3 - - def append(self, s): - raise NotImplementedError - -class OverflowableBuffer(object): - """ - This buffer implementation has four stages: - - No data - - Bytes-based buffer - - BytesIO-based buffer - - Temporary file storage - The first two stages are fastest for simple transfers. - """ - - overflowed = False - buf = None - strbuf = b'' # Bytes-based buffer. - - def __init__(self, overflow): - # overflow is the maximum to be stored in a StringIO buffer. - self.overflow = overflow - - def __len__(self): - buf = self.buf - if buf is not None: - # use buf.__len__ rather than len(buf) FBO of not getting - # OverflowError on Python 2 - return buf.__len__() - else: - return self.strbuf.__len__() - - def __nonzero__(self): - # use self.__len__ rather than len(self) FBO of not getting - # OverflowError on Python 2 - return self.__len__() > 0 - - __bool__ = __nonzero__ # py3 - - def _create_buffer(self): - strbuf = self.strbuf - if len(strbuf) >= self.overflow: - self._set_large_buffer() - else: - self._set_small_buffer() - buf = self.buf - if strbuf: - buf.append(self.strbuf) - self.strbuf = b'' - return buf - - def _set_small_buffer(self): - self.buf = BytesIOBasedBuffer(self.buf) - self.overflowed = False - - def _set_large_buffer(self): - self.buf = TempfileBasedBuffer(self.buf) - self.overflowed = True - - def append(self, s): - buf = self.buf - if buf is None: - strbuf = self.strbuf - if len(strbuf) + len(s) < STRBUF_LIMIT: - self.strbuf = strbuf + s - return - buf = self._create_buffer() - buf.append(s) - # use buf.__len__ rather than len(buf) FBO of not getting - # OverflowError on Python 2 - sz = buf.__len__() - if not self.overflowed: - if sz >= self.overflow: - self._set_large_buffer() - - def get(self, numbytes=-1, skip=False): - buf = self.buf - if buf is None: - strbuf = self.strbuf - if not skip: - return strbuf - buf = self._create_buffer() - return buf.get(numbytes, skip) - - def skip(self, numbytes, allow_prune=False): - buf = self.buf - if buf is None: - if allow_prune and numbytes == len(self.strbuf): - # We could slice instead of converting to - # a buffer, but that would eat up memory in - # large transfers. - self.strbuf = b'' - return - buf = self._create_buffer() - buf.skip(numbytes, allow_prune) - - def prune(self): - """ - A potentially expensive operation that removes all data - already retrieved from the buffer. - """ - buf = self.buf - if buf is None: - self.strbuf = b'' - return - buf.prune() - if self.overflowed: - # use buf.__len__ rather than len(buf) FBO of not getting - # OverflowError on Python 2 - sz = buf.__len__() - if sz < self.overflow: - # Revert to a faster buffer. - self._set_small_buffer() - - def getfile(self): - buf = self.buf - if buf is None: - buf = self._create_buffer() - return buf.getfile() - - def close(self): - buf = self.buf - if buf is not None: - buf.close() diff --git a/libs/waitress/channel.py b/libs/waitress/channel.py deleted file mode 100644 index ca0251126..000000000 --- a/libs/waitress/channel.py +++ /dev/null @@ -1,386 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001, 2002 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -import asyncore -import socket -import threading -import time -import traceback - -from waitress.buffers import ( - OverflowableBuffer, - ReadOnlyFileBasedBuffer, -) - -from waitress.parser import HTTPRequestParser - -from waitress.task import ( - ErrorTask, - WSGITask, -) - -from waitress.utilities import ( - logging_dispatcher, - InternalServerError, -) - -class HTTPChannel(logging_dispatcher, object): - """ - Setting self.requests = [somerequest] prevents more requests from being - received until the out buffers have been flushed. - - Setting self.requests = [] allows more requests to be received. - """ - - task_class = WSGITask - error_task_class = ErrorTask - parser_class = HTTPRequestParser - - request = None # A request parser instance - last_activity = 0 # Time of last activity - will_close = False # set to True to close the socket. - close_when_flushed = False # set to True to close the socket when flushed - requests = () # currently pending requests - sent_continue = False # used as a latch after sending 100 continue - force_flush = False # indicates a need to flush the outbuf - - # - # ASYNCHRONOUS METHODS (including __init__) - # - - def __init__( - self, - server, - sock, - addr, - adj, - map=None, - ): - self.server = server - self.adj = adj - self.outbufs = [OverflowableBuffer(adj.outbuf_overflow)] - self.creation_time = self.last_activity = time.time() - - # task_lock used to push/pop requests - self.task_lock = threading.Lock() - # outbuf_lock used to access any outbuf - self.outbuf_lock = threading.Lock() - - asyncore.dispatcher.__init__(self, sock, map=map) - - # Don't let asyncore.dispatcher throttle self.addr on us. - self.addr = addr - - def any_outbuf_has_data(self): - for outbuf in self.outbufs: - if bool(outbuf): - return True - return False - - def total_outbufs_len(self): - # genexpr == more funccalls - # use b.__len__ rather than len(b) FBO of not getting OverflowError - # on Python 2 - return sum([b.__len__() for b in self.outbufs]) - - def writable(self): - # if there's data in the out buffer or we've been instructed to close - # the channel (possibly by our server maintenance logic), run - # handle_write - return self.any_outbuf_has_data() or self.will_close - - def handle_write(self): - # Precondition: there's data in the out buffer to be sent, or - # there's a pending will_close request - if not self.connected: - # we dont want to close the channel twice - return - - # try to flush any pending output - if not self.requests: - # 1. There are no running tasks, so we don't need to try to lock - # the outbuf before sending - # 2. The data in the out buffer should be sent as soon as possible - # because it's either data left over from task output - # or a 100 Continue line sent within "received". - flush = self._flush_some - elif self.force_flush: - # 1. There's a running task, so we need to try to lock - # the outbuf before sending - # 2. This is the last chunk sent by the Nth of M tasks in a - # sequence on this channel, so flush it regardless of whether - # it's >= self.adj.send_bytes. We need to do this now, or it - # won't get done. - flush = self._flush_some_if_lockable - self.force_flush = False - elif (self.total_outbufs_len() >= self.adj.send_bytes): - # 1. There's a running task, so we need to try to lock - # the outbuf before sending - # 2. Only try to send if the data in the out buffer is larger - # than self.adj_bytes to avoid TCP fragmentation - flush = self._flush_some_if_lockable - else: - # 1. There's not enough data in the out buffer to bother to send - # right now. - flush = None - - if flush: - try: - flush() - except socket.error: - if self.adj.log_socket_errors: - self.logger.exception('Socket error') - self.will_close = True - except: - self.logger.exception('Unexpected exception when flushing') - self.will_close = True - - if self.close_when_flushed and not self.any_outbuf_has_data(): - self.close_when_flushed = False - self.will_close = True - - if self.will_close: - self.handle_close() - - def readable(self): - # We might want to create a new task. We can only do this if: - # 1. We're not already about to close the connection. - # 2. There's no already currently running task(s). - # 3. There's no data in the output buffer that needs to be sent - # before we potentially create a new task. - return not (self.will_close or self.requests or - self.any_outbuf_has_data()) - - def handle_read(self): - try: - data = self.recv(self.adj.recv_bytes) - except socket.error: - if self.adj.log_socket_errors: - self.logger.exception('Socket error') - self.handle_close() - return - if data: - self.last_activity = time.time() - self.received(data) - - def received(self, data): - """ - Receives input asynchronously and assigns one or more requests to the - channel. - """ - # Preconditions: there's no task(s) already running - request = self.request - requests = [] - - if not data: - return False - - while data: - if request is None: - request = self.parser_class(self.adj) - n = request.received(data) - if request.expect_continue and request.headers_finished: - # guaranteed by parser to be a 1.1 request - request.expect_continue = False - if not self.sent_continue: - # there's no current task, so we don't need to try to - # lock the outbuf to append to it. - self.outbufs[-1].append(b'HTTP/1.1 100 Continue\r\n\r\n') - self.sent_continue = True - self._flush_some() - request.completed = False - if request.completed: - # The request (with the body) is ready to use. - self.request = None - if not request.empty: - requests.append(request) - request = None - else: - self.request = request - if n >= len(data): - break - data = data[n:] - - if requests: - self.requests = requests - self.server.add_task(self) - - return True - - def _flush_some_if_lockable(self): - # Since our task may be appending to the outbuf, we try to acquire - # the lock, but we don't block if we can't. - locked = self.outbuf_lock.acquire(False) - if locked: - try: - self._flush_some() - finally: - self.outbuf_lock.release() - - def _flush_some(self): - # Send as much data as possible to our client - - sent = 0 - dobreak = False - - while True: - outbuf = self.outbufs[0] - # use outbuf.__len__ rather than len(outbuf) FBO of not getting - # OverflowError on Python 2 - outbuflen = outbuf.__len__() - if outbuflen <= 0: - # self.outbufs[-1] must always be a writable outbuf - if len(self.outbufs) > 1: - toclose = self.outbufs.pop(0) - try: - toclose.close() - except: - self.logger.exception( - 'Unexpected error when closing an outbuf') - continue # pragma: no cover (coverage bug, it is hit) - else: - if hasattr(outbuf, 'prune'): - outbuf.prune() - dobreak = True - - while outbuflen > 0: - chunk = outbuf.get(self.adj.send_bytes) - num_sent = self.send(chunk) - if num_sent: - outbuf.skip(num_sent, True) - outbuflen -= num_sent - sent += num_sent - else: - dobreak = True - break - - if dobreak: - break - - if sent: - self.last_activity = time.time() - return True - - return False - - def handle_close(self): - for outbuf in self.outbufs: - try: - outbuf.close() - except: - self.logger.exception( - 'Unknown exception while trying to close outbuf') - self.connected = False - asyncore.dispatcher.close(self) - - def add_channel(self, map=None): - """See asyncore.dispatcher - - This hook keeps track of opened channels. - """ - asyncore.dispatcher.add_channel(self, map) - self.server.active_channels[self._fileno] = self - - def del_channel(self, map=None): - """See asyncore.dispatcher - - This hook keeps track of closed channels. - """ - fd = self._fileno # next line sets this to None - asyncore.dispatcher.del_channel(self, map) - ac = self.server.active_channels - if fd in ac: - del ac[fd] - - # - # SYNCHRONOUS METHODS - # - - def write_soon(self, data): - if data: - # the async mainloop might be popping data off outbuf; we can - # block here waiting for it because we're in a task thread - with self.outbuf_lock: - if data.__class__ is ReadOnlyFileBasedBuffer: - # they used wsgi.file_wrapper - self.outbufs.append(data) - nextbuf = OverflowableBuffer(self.adj.outbuf_overflow) - self.outbufs.append(nextbuf) - else: - self.outbufs[-1].append(data) - # XXX We might eventually need to pull the trigger here (to - # instruct select to stop blocking), but it slows things down so - # much that I'll hold off for now; "server push" on otherwise - # unbusy systems may suffer. - return len(data) - return 0 - - def service(self): - """Execute all pending requests """ - with self.task_lock: - while self.requests: - request = self.requests[0] - if request.error: - task = self.error_task_class(self, request) - else: - task = self.task_class(self, request) - try: - task.service() - except: - self.logger.exception('Exception when serving %s' % - task.request.path) - if not task.wrote_header: - if self.adj.expose_tracebacks: - body = traceback.format_exc() - else: - body = ('The server encountered an unexpected ' - 'internal server error') - req_version = request.version - req_headers = request.headers - request = self.parser_class(self.adj) - request.error = InternalServerError(body) - # copy some original request attributes to fulfill - # HTTP 1.1 requirements - request.version = req_version - try: - request.headers['CONNECTION'] = req_headers[ - 'CONNECTION'] - except KeyError: - pass - task = self.error_task_class(self, request) - task.service() # must not fail - else: - task.close_on_finish = True - # we cannot allow self.requests to drop to empty til - # here; otherwise the mainloop gets confused - if task.close_on_finish: - self.close_when_flushed = True - for request in self.requests: - request.close() - self.requests = [] - else: - request = self.requests.pop(0) - request.close() - - self.force_flush = True - self.server.pull_trigger() - self.last_activity = time.time() - - def cancel(self): - """ Cancels all pending requests """ - self.force_flush = True - self.last_activity = time.time() - self.requests = [] - - def defer(self): - pass diff --git a/libs/waitress/compat.py b/libs/waitress/compat.py deleted file mode 100644 index 700f7a1e7..000000000 --- a/libs/waitress/compat.py +++ /dev/null @@ -1,140 +0,0 @@ -import sys -import types -import platform -import warnings - -try: - import urlparse -except ImportError: # pragma: no cover - from urllib import parse as urlparse - -# True if we are running on Python 3. -PY2 = sys.version_info[0] == 2 -PY3 = sys.version_info[0] == 3 - -# True if we are running on Windows -WIN = platform.system() == 'Windows' - -if PY3: # pragma: no cover - string_types = str, - integer_types = int, - class_types = type, - text_type = str - binary_type = bytes - long = int -else: - string_types = basestring, - integer_types = (int, long) - class_types = (type, types.ClassType) - text_type = unicode - binary_type = str - long = long - -if PY3: # pragma: no cover - from urllib.parse import unquote_to_bytes - def unquote_bytes_to_wsgi(bytestring): - return unquote_to_bytes(bytestring).decode('latin-1') -else: - from urlparse import unquote as unquote_to_bytes - def unquote_bytes_to_wsgi(bytestring): - return unquote_to_bytes(bytestring) - -def text_(s, encoding='latin-1', errors='strict'): - """ If ``s`` is an instance of ``binary_type``, return - ``s.decode(encoding, errors)``, otherwise return ``s``""" - if isinstance(s, binary_type): - return s.decode(encoding, errors) - return s # pragma: no cover - -if PY3: # pragma: no cover - def tostr(s): - if isinstance(s, text_type): - s = s.encode('latin-1') - return str(s, 'latin-1', 'strict') - - def tobytes(s): - return bytes(s, 'latin-1') -else: - tostr = str - - def tobytes(s): - return s - -try: - from Queue import ( - Queue, - Empty, - ) -except ImportError: # pragma: no cover - from queue import ( - Queue, - Empty, - ) - -if PY3: # pragma: no cover - import builtins - exec_ = getattr(builtins, "exec") - - def reraise(tp, value, tb=None): - if value is None: - value = tp - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value - - del builtins - -else: # pragma: no cover - def exec_(code, globs=None, locs=None): - """Execute code in a namespace.""" - if globs is None: - frame = sys._getframe(1) - globs = frame.f_globals - if locs is None: - locs = frame.f_locals - del frame - elif locs is None: - locs = globs - exec("""exec code in globs, locs""") - - exec_("""def reraise(tp, value, tb=None): - raise tp, value, tb -""") - -try: - from StringIO import StringIO as NativeIO -except ImportError: # pragma: no cover - from io import StringIO as NativeIO - -try: - import httplib -except ImportError: # pragma: no cover - from http import client as httplib - -try: - MAXINT = sys.maxint -except AttributeError: # pragma: no cover - MAXINT = sys.maxsize - - -# Fix for issue reported in https://github.com/Pylons/waitress/issues/138, -# Python on Windows may not define IPPROTO_IPV6 in socket. -import socket - -HAS_IPV6 = socket.has_ipv6 - -if hasattr(socket, 'IPPROTO_IPV6') and hasattr(socket, 'IPV6_V6ONLY'): - IPPROTO_IPV6 = socket.IPPROTO_IPV6 - IPV6_V6ONLY = socket.IPV6_V6ONLY -else: # pragma: no cover - if WIN: - IPPROTO_IPV6 = 41 - IPV6_V6ONLY = 27 - else: - warnings.warn( - 'OS does not support required IPv6 socket flags. This is requirement ' - 'for Waitress. Please open an issue at https://github.com/Pylons/waitress. ' - 'IPv6 support has been disabled.', - RuntimeWarning - ) - HAS_IPV6 = False diff --git a/libs/waitress/parser.py b/libs/waitress/parser.py deleted file mode 100644 index 6d2f3409d..000000000 --- a/libs/waitress/parser.py +++ /dev/null @@ -1,313 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001, 2002 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -"""HTTP Request Parser - -This server uses asyncore to accept connections and do initial -processing but threads to do work. -""" -import re -from io import BytesIO - -from waitress.compat import ( - tostr, - urlparse, - unquote_bytes_to_wsgi, -) - -from waitress.buffers import OverflowableBuffer - -from waitress.receiver import ( - FixedStreamReceiver, - ChunkedReceiver, -) - -from waitress.utilities import ( - find_double_newline, - RequestEntityTooLarge, - RequestHeaderFieldsTooLarge, - BadRequest, -) - -class ParsingError(Exception): - pass - -class HTTPRequestParser(object): - """A structure that collects the HTTP request. - - Once the stream is completed, the instance is passed to - a server task constructor. - """ - completed = False # Set once request is completed. - empty = False # Set if no request was made. - expect_continue = False # client sent "Expect: 100-continue" header - headers_finished = False # True when headers have been read - header_plus = b'' - chunked = False - content_length = 0 - header_bytes_received = 0 - body_bytes_received = 0 - body_rcv = None - version = '1.0' - error = None - connection_close = False - - # Other attributes: first_line, header, headers, command, uri, version, - # path, query, fragment - - def __init__(self, adj): - """ - adj is an Adjustments object. - """ - # headers is a mapping containing keys translated to uppercase - # with dashes turned into underscores. - self.headers = {} - self.adj = adj - - def received(self, data): - """ - Receives the HTTP stream for one request. Returns the number of - bytes consumed. Sets the completed flag once both the header and the - body have been received. - """ - if self.completed: - return 0 # Can't consume any more. - datalen = len(data) - br = self.body_rcv - if br is None: - # In header. - s = self.header_plus + data - index = find_double_newline(s) - if index >= 0: - # Header finished. - header_plus = s[:index] - consumed = len(data) - (len(s) - index) - # Remove preceeding blank lines. - header_plus = header_plus.lstrip() - if not header_plus: - self.empty = True - self.completed = True - else: - try: - self.parse_header(header_plus) - except ParsingError as e: - self.error = BadRequest(e.args[0]) - self.completed = True - else: - if self.body_rcv is None: - # no content-length header and not a t-e: chunked - # request - self.completed = True - if self.content_length > 0: - max_body = self.adj.max_request_body_size - # we won't accept this request if the content-length - # is too large - if self.content_length >= max_body: - self.error = RequestEntityTooLarge( - 'exceeds max_body of %s' % max_body) - self.completed = True - self.headers_finished = True - return consumed - else: - # Header not finished yet. - self.header_bytes_received += datalen - max_header = self.adj.max_request_header_size - if self.header_bytes_received >= max_header: - # malformed header, we need to construct some request - # on our own. we disregard the incoming(?) requests HTTP - # version and just use 1.0. IOW someone just sent garbage - # over the wire - self.parse_header(b'GET / HTTP/1.0\n') - self.error = RequestHeaderFieldsTooLarge( - 'exceeds max_header of %s' % max_header) - self.completed = True - self.header_plus = s - return datalen - else: - # In body. - consumed = br.received(data) - self.body_bytes_received += consumed - max_body = self.adj.max_request_body_size - if self.body_bytes_received >= max_body: - # this will only be raised during t-e: chunked requests - self.error = RequestEntityTooLarge( - 'exceeds max_body of %s' % max_body) - self.completed = True - elif br.error: - # garbage in chunked encoding input probably - self.error = br.error - self.completed = True - elif br.completed: - # The request (with the body) is ready to use. - self.completed = True - if self.chunked: - # We've converted the chunked transfer encoding request - # body into a normal request body, so we know its content - # length; set the header here. We already popped the - # TRANSFER_ENCODING header in parse_header, so this will - # appear to the client to be an entirely non-chunked HTTP - # request with a valid content-length. - self.headers['CONTENT_LENGTH'] = str(br.__len__()) - return consumed - - def parse_header(self, header_plus): - """ - Parses the header_plus block of text (the headers plus the - first line of the request). - """ - index = header_plus.find(b'\n') - if index >= 0: - first_line = header_plus[:index].rstrip() - header = header_plus[index + 1:] - else: - first_line = header_plus.rstrip() - header = b'' - - self.first_line = first_line # for testing - - lines = get_header_lines(header) - - headers = self.headers - for line in lines: - index = line.find(b':') - if index > 0: - key = line[:index] - if b'_' in key: - continue - value = line[index + 1:].strip() - key1 = tostr(key.upper().replace(b'-', b'_')) - # If a header already exists, we append subsequent values - # seperated by a comma. Applications already need to handle - # the comma seperated values, as HTTP front ends might do - # the concatenation for you (behavior specified in RFC2616). - try: - headers[key1] += tostr(b', ' + value) - except KeyError: - headers[key1] = tostr(value) - # else there's garbage in the headers? - - # command, uri, version will be bytes - command, uri, version = crack_first_line(first_line) - version = tostr(version) - command = tostr(command) - self.command = command - self.version = version - (self.proxy_scheme, - self.proxy_netloc, - self.path, - self.query, self.fragment) = split_uri(uri) - self.url_scheme = self.adj.url_scheme - connection = headers.get('CONNECTION', '') - - if version == '1.0': - if connection.lower() != 'keep-alive': - self.connection_close = True - - if version == '1.1': - # since the server buffers data from chunked transfers and clients - # never need to deal with chunked requests, downstream clients - # should not see the HTTP_TRANSFER_ENCODING header; we pop it - # here - te = headers.pop('TRANSFER_ENCODING', '') - if te.lower() == 'chunked': - self.chunked = True - buf = OverflowableBuffer(self.adj.inbuf_overflow) - self.body_rcv = ChunkedReceiver(buf) - expect = headers.get('EXPECT', '').lower() - self.expect_continue = expect == '100-continue' - if connection.lower() == 'close': - self.connection_close = True - - if not self.chunked: - try: - cl = int(headers.get('CONTENT_LENGTH', 0)) - except ValueError: - cl = 0 - self.content_length = cl - if cl > 0: - buf = OverflowableBuffer(self.adj.inbuf_overflow) - self.body_rcv = FixedStreamReceiver(cl, buf) - - def get_body_stream(self): - body_rcv = self.body_rcv - if body_rcv is not None: - return body_rcv.getfile() - else: - return BytesIO() - - def close(self): - body_rcv = self.body_rcv - if body_rcv is not None: - body_rcv.getbuf().close() - -def split_uri(uri): - # urlsplit handles byte input by returning bytes on py3, so - # scheme, netloc, path, query, and fragment are bytes - try: - scheme, netloc, path, query, fragment = urlparse.urlsplit(uri) - except UnicodeError: - raise ParsingError('Bad URI') - return ( - tostr(scheme), - tostr(netloc), - unquote_bytes_to_wsgi(path), - tostr(query), - tostr(fragment), - ) - -def get_header_lines(header): - """ - Splits the header into lines, putting multi-line headers together. - """ - r = [] - lines = header.split(b'\n') - for line in lines: - if line.startswith((b' ', b'\t')): - if not r: - # http://corte.si/posts/code/pathod/pythonservers/index.html - raise ParsingError('Malformed header line "%s"' % tostr(line)) - r[-1] += line - else: - r.append(line) - return r - -first_line_re = re.compile( - b'([^ ]+) ' - b'((?:[^ :?#]+://[^ ?#/]*(?:[0-9]{1,5})?)?[^ ]+)' - b'(( HTTP/([0-9.]+))$|$)' -) - -def crack_first_line(line): - m = first_line_re.match(line) - if m is not None and m.end() == len(line): - if m.group(3): - version = m.group(5) - else: - version = None - method = m.group(1) - - # the request methods that are currently defined are all uppercase: - # https://www.iana.org/assignments/http-methods/http-methods.xhtml and - # the request method is case sensitive according to - # https://tools.ietf.org/html/rfc7231#section-4.1 - - # By disallowing anything but uppercase methods we save poor - # unsuspecting souls from sending lowercase HTTP methods to waitress - # and having the request complete, while servers like nginx drop the - # request onto the floor. - if method != method.upper(): - raise ParsingError('Malformed HTTP method "%s"' % tostr(method)) - uri = m.group(2) - return method, uri, version - else: - return b'', b'', b'' diff --git a/libs/waitress/receiver.py b/libs/waitress/receiver.py deleted file mode 100644 index 594ae971d..000000000 --- a/libs/waitress/receiver.py +++ /dev/null @@ -1,149 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001, 2002 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -"""Data Chunk Receiver -""" - -from waitress.utilities import find_double_newline - -from waitress.utilities import BadRequest - -class FixedStreamReceiver(object): - - # See IStreamConsumer - completed = False - error = None - - def __init__(self, cl, buf): - self.remain = cl - self.buf = buf - - def __len__(self): - return self.buf.__len__() - - def received(self, data): - 'See IStreamConsumer' - rm = self.remain - if rm < 1: - self.completed = True # Avoid any chance of spinning - return 0 - datalen = len(data) - if rm <= datalen: - self.buf.append(data[:rm]) - self.remain = 0 - self.completed = True - return rm - else: - self.buf.append(data) - self.remain -= datalen - return datalen - - def getfile(self): - return self.buf.getfile() - - def getbuf(self): - return self.buf - -class ChunkedReceiver(object): - - chunk_remainder = 0 - control_line = b'' - all_chunks_received = False - trailer = b'' - completed = False - error = None - - # max_control_line = 1024 - # max_trailer = 65536 - - def __init__(self, buf): - self.buf = buf - - def __len__(self): - return self.buf.__len__() - - def received(self, s): - # Returns the number of bytes consumed. - if self.completed: - return 0 - orig_size = len(s) - while s: - rm = self.chunk_remainder - if rm > 0: - # Receive the remainder of a chunk. - to_write = s[:rm] - self.buf.append(to_write) - written = len(to_write) - s = s[written:] - self.chunk_remainder -= written - elif not self.all_chunks_received: - # Receive a control line. - s = self.control_line + s - pos = s.find(b'\n') - if pos < 0: - # Control line not finished. - self.control_line = s - s = '' - else: - # Control line finished. - line = s[:pos] - s = s[pos + 1:] - self.control_line = b'' - line = line.strip() - if line: - # Begin a new chunk. - semi = line.find(b';') - if semi >= 0: - # discard extension info. - line = line[:semi] - try: - sz = int(line.strip(), 16) # hexadecimal - except ValueError: # garbage in input - self.error = BadRequest( - 'garbage in chunked encoding input') - sz = 0 - if sz > 0: - # Start a new chunk. - self.chunk_remainder = sz - else: - # Finished chunks. - self.all_chunks_received = True - # else expect a control line. - else: - # Receive the trailer. - trailer = self.trailer + s - if trailer.startswith(b'\r\n'): - # No trailer. - self.completed = True - return orig_size - (len(trailer) - 2) - elif trailer.startswith(b'\n'): - # No trailer. - self.completed = True - return orig_size - (len(trailer) - 1) - pos = find_double_newline(trailer) - if pos < 0: - # Trailer not finished. - self.trailer = trailer - s = b'' - else: - # Finished the trailer. - self.completed = True - self.trailer = trailer[:pos] - return orig_size - (len(trailer) - pos) - return orig_size - - def getfile(self): - return self.buf.getfile() - - def getbuf(self): - return self.buf diff --git a/libs/waitress/runner.py b/libs/waitress/runner.py deleted file mode 100644 index abdb38e87..000000000 --- a/libs/waitress/runner.py +++ /dev/null @@ -1,275 +0,0 @@ -############################################################################## -# -# Copyright (c) 2013 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -"""Command line runner. -""" - -from __future__ import print_function, unicode_literals - -import getopt -import os -import os.path -import re -import sys - -from waitress import serve -from waitress.adjustments import Adjustments - -HELP = """\ -Usage: - - {0} [OPTS] MODULE:OBJECT - -Standard options: - - --help - Show this information. - - --call - Call the given object to get the WSGI application. - - --host=ADDR - Hostname or IP address on which to listen, default is '0.0.0.0', - which means "all IP addresses on this host". - - Note: May not be used together with --listen - - --port=PORT - TCP port on which to listen, default is '8080' - - Note: May not be used together with --listen - - --listen=ip:port - Tell waitress to listen on an ip port combination. - - Example: - - --listen=127.0.0.1:8080 - --listen=[::1]:8080 - --listen=*:8080 - - This option may be used multiple times to listen on multipe sockets. - A wildcard for the hostname is also supported and will bind to both - IPv4/IPv6 depending on whether they are enabled or disabled. - - --[no-]ipv4 - Toggle on/off IPv4 support. - - Example: - - --no-ipv4 - - This will disable IPv4 socket support. This affects wildcard matching - when generating the list of sockets. - - --[no-]ipv6 - Toggle on/off IPv6 support. - - Example: - - --no-ipv6 - - This will turn on IPv6 socket support. This affects wildcard matching - when generating a list of sockets. - - --unix-socket=PATH - Path of Unix socket. If a socket path is specified, a Unix domain - socket is made instead of the usual inet domain socket. - - Not available on Windows. - - --unix-socket-perms=PERMS - Octal permissions to use for the Unix domain socket, default is - '600'. - - --url-scheme=STR - Default wsgi.url_scheme value, default is 'http'. - - --url-prefix=STR - The ``SCRIPT_NAME`` WSGI environment value. Setting this to anything - except the empty string will cause the WSGI ``SCRIPT_NAME`` value to be - the value passed minus any trailing slashes you add, and it will cause - the ``PATH_INFO`` of any request which is prefixed with this value to - be stripped of the prefix. Default is the empty string. - - --ident=STR - Server identity used in the 'Server' header in responses. Default - is 'waitress'. - -Tuning options: - - --threads=INT - Number of threads used to process application logic, default is 4. - - --backlog=INT - Connection backlog for the server. Default is 1024. - - --recv-bytes=INT - Number of bytes to request when calling socket.recv(). Default is - 8192. - - --send-bytes=INT - Number of bytes to send to socket.send(). Default is 18000. - Multiples of 9000 should avoid partly-filled TCP packets. - - --outbuf-overflow=INT - A temporary file should be created if the pending output is larger - than this. Default is 1048576 (1MB). - - --inbuf-overflow=INT - A temporary file should be created if the pending input is larger - than this. Default is 524288 (512KB). - - --connection-limit=INT - Stop creating new channelse if too many are already active. - Default is 100. - - --cleanup-interval=INT - Minimum seconds between cleaning up inactive channels. Default - is 30. See '--channel-timeout'. - - --channel-timeout=INT - Maximum number of seconds to leave inactive connections open. - Default is 120. 'Inactive' is defined as 'has recieved no data - from the client and has sent no data to the client'. - - --[no-]log-socket-errors - Toggle whether premature client disconnect tracepacks ought to be - logged. On by default. - - --max-request-header-size=INT - Maximum size of all request headers combined. Default is 262144 - (256KB). - - --max-request-body-size=INT - Maximum size of request body. Default is 1073741824 (1GB). - - --[no-]expose-tracebacks - Toggle whether to expose tracebacks of unhandled exceptions to the - client. Off by default. - - --asyncore-loop-timeout=INT - The timeout value in seconds passed to asyncore.loop(). Default is 1. - - --asyncore-use-poll - The use_poll argument passed to ``asyncore.loop()``. Helps overcome - open file descriptors limit. Default is False. - -""" - -RUNNER_PATTERN = re.compile(r""" - ^ - (?P - [a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)* - ) - : - (?P - [a-z_][a-z0-9_]*(?:\.[a-z_][a-z0-9_]*)* - ) - $ - """, re.I | re.X) - -def match(obj_name): - matches = RUNNER_PATTERN.match(obj_name) - if not matches: - raise ValueError("Malformed application '{0}'".format(obj_name)) - return matches.group('module'), matches.group('object') - -def resolve(module_name, object_name): - """Resolve a named object in a module.""" - # We cast each segments due to an issue that has been found to manifest - # in Python 2.6.6, but not 2.6.8, and may affect other revisions of Python - # 2.6 and 2.7, whereby ``__import__`` chokes if the list passed in the - # ``fromlist`` argument are unicode strings rather than 8-bit strings. - # The error triggered is "TypeError: Item in ``fromlist '' not a string". - # My guess is that this was fixed by checking against ``basestring`` - # rather than ``str`` sometime between the release of 2.6.6 and 2.6.8, - # but I've yet to go over the commits. I know, however, that the NEWS - # file makes no mention of such a change to the behaviour of - # ``__import__``. - segments = [str(segment) for segment in object_name.split('.')] - obj = __import__(module_name, fromlist=segments[:1]) - for segment in segments: - obj = getattr(obj, segment) - return obj - -def show_help(stream, name, error=None): # pragma: no cover - if error is not None: - print('Error: {0}\n'.format(error), file=stream) - print(HELP.format(name), file=stream) - -def show_exception(stream): - exc_type, exc_value = sys.exc_info()[:2] - args = getattr(exc_value, 'args', None) - print( - ( - 'There was an exception ({0}) importing your module.\n' - ).format( - exc_type.__name__, - ), - file=stream - ) - if args: - print('It had these arguments: ', file=stream) - for idx, arg in enumerate(args, start=1): - print('{0}. {1}\n'.format(idx, arg), file=stream) - else: - print('It had no arguments.', file=stream) - -def run(argv=sys.argv, _serve=serve): - """Command line runner.""" - name = os.path.basename(argv[0]) - - try: - kw, args = Adjustments.parse_args(argv[1:]) - except getopt.GetoptError as exc: - show_help(sys.stderr, name, str(exc)) - return 1 - - if kw['help']: - show_help(sys.stdout, name) - return 0 - - if len(args) != 1: - show_help(sys.stderr, name, 'Specify one application only') - return 1 - - try: - module, obj_name = match(args[0]) - except ValueError as exc: - show_help(sys.stderr, name, str(exc)) - show_exception(sys.stderr) - return 1 - - # Add the current directory onto sys.path - sys.path.append(os.getcwd()) - - # Get the WSGI function. - try: - app = resolve(module, obj_name) - except ImportError: - show_help(sys.stderr, name, "Bad module '{0}'".format(module)) - show_exception(sys.stderr) - return 1 - except AttributeError: - show_help(sys.stderr, name, "Bad object name '{0}'".format(obj_name)) - show_exception(sys.stderr) - return 1 - if kw['call']: - app = app() - - # These arguments are specific to the runner, not waitress itself. - del kw['call'], kw['help'] - - _serve(app, **kw) - return 0 diff --git a/libs/waitress/server.py b/libs/waitress/server.py deleted file mode 100644 index 79aa9b75d..000000000 --- a/libs/waitress/server.py +++ /dev/null @@ -1,357 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001, 2002 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## - -import asyncore -import os -import os.path -import socket -import time - -from waitress import trigger -from waitress.adjustments import Adjustments -from waitress.channel import HTTPChannel -from waitress.task import ThreadedTaskDispatcher -from waitress.utilities import ( - cleanup_unix_socket, - logging_dispatcher, - ) -from waitress.compat import ( - IPPROTO_IPV6, - IPV6_V6ONLY, - ) - -def create_server(application, - map=None, - _start=True, # test shim - _sock=None, # test shim - _dispatcher=None, # test shim - **kw # adjustments - ): - """ - if __name__ == '__main__': - server = create_server(app) - server.run() - """ - if application is None: - raise ValueError( - 'The "app" passed to ``create_server`` was ``None``. You forgot ' - 'to return a WSGI app within your application.' - ) - adj = Adjustments(**kw) - - if map is None: # pragma: nocover - map = {} - - dispatcher = _dispatcher - if dispatcher is None: - dispatcher = ThreadedTaskDispatcher() - dispatcher.set_thread_count(adj.threads) - - if adj.unix_socket and hasattr(socket, 'AF_UNIX'): - sockinfo = (socket.AF_UNIX, socket.SOCK_STREAM, None, None) - return UnixWSGIServer( - application, - map, - _start, - _sock, - dispatcher=dispatcher, - adj=adj, - sockinfo=sockinfo) - - effective_listen = [] - last_serv = None - for sockinfo in adj.listen: - # When TcpWSGIServer is called, it registers itself in the map. This - # side-effect is all we need it for, so we don't store a reference to - # or return it to the user. - last_serv = TcpWSGIServer( - application, - map, - _start, - _sock, - dispatcher=dispatcher, - adj=adj, - sockinfo=sockinfo) - effective_listen.append((last_serv.effective_host, last_serv.effective_port)) - - # We are running a single server, so we can just return the last server, - # saves us from having to create one more object - if len(adj.listen) == 1: - # In this case we have no need to use a MultiSocketServer - return last_serv - - # Return a class that has a utility function to print out the sockets it's - # listening on, and has a .run() function. All of the TcpWSGIServers - # registered themselves in the map above. - return MultiSocketServer(map, adj, effective_listen, dispatcher) - - -# This class is only ever used if we have multiple listen sockets. It allows -# the serve() API to call .run() which starts the asyncore loop, and catches -# SystemExit/KeyboardInterrupt so that it can atempt to cleanly shut down. -class MultiSocketServer(object): - asyncore = asyncore # test shim - - def __init__(self, - map=None, - adj=None, - effective_listen=None, - dispatcher=None, - ): - self.adj = adj - self.map = map - self.effective_listen = effective_listen - self.task_dispatcher = dispatcher - - def print_listen(self, format_str): # pragma: nocover - for l in self.effective_listen: - l = list(l) - - if ':' in l[0]: - l[0] = '[{}]'.format(l[0]) - - print(format_str.format(*l)) - - def run(self): - try: - self.asyncore.loop( - timeout=self.adj.asyncore_loop_timeout, - map=self.map, - use_poll=self.adj.asyncore_use_poll, - ) - except (SystemExit, KeyboardInterrupt): - self.task_dispatcher.shutdown() - - -class BaseWSGIServer(logging_dispatcher, object): - - channel_class = HTTPChannel - next_channel_cleanup = 0 - socketmod = socket # test shim - asyncore = asyncore # test shim - - def __init__(self, - application, - map=None, - _start=True, # test shim - _sock=None, # test shim - dispatcher=None, # dispatcher - adj=None, # adjustments - sockinfo=None, # opaque object - **kw - ): - if adj is None: - adj = Adjustments(**kw) - if map is None: - # use a nonglobal socket map by default to hopefully prevent - # conflicts with apps and libs that use the asyncore global socket - # map ala https://github.com/Pylons/waitress/issues/63 - map = {} - if sockinfo is None: - sockinfo = adj.listen[0] - - self.sockinfo = sockinfo - self.family = sockinfo[0] - self.socktype = sockinfo[1] - self.application = application - self.adj = adj - self.trigger = trigger.trigger(map) - if dispatcher is None: - dispatcher = ThreadedTaskDispatcher() - dispatcher.set_thread_count(self.adj.threads) - - self.task_dispatcher = dispatcher - self.asyncore.dispatcher.__init__(self, _sock, map=map) - if _sock is None: - self.create_socket(self.family, self.socktype) - if self.family == socket.AF_INET6: # pragma: nocover - self.socket.setsockopt(IPPROTO_IPV6, IPV6_V6ONLY, 1) - - self.set_reuse_addr() - self.bind_server_socket() - self.effective_host, self.effective_port = self.getsockname() - self.server_name = self.get_server_name(self.effective_host) - self.active_channels = {} - if _start: - self.accept_connections() - - def bind_server_socket(self): - raise NotImplementedError # pragma: no cover - - def get_server_name(self, ip): - """Given an IP or hostname, try to determine the server name.""" - if ip: - server_name = str(ip) - else: - server_name = str(self.socketmod.gethostname()) - - # Convert to a host name if necessary. - for c in server_name: - if c != '.' and not c.isdigit(): - return server_name - try: - if server_name == '0.0.0.0' or server_name == '::': - return 'localhost' - server_name = self.socketmod.gethostbyaddr(server_name)[0] - except socket.error: # pragma: no cover - pass - return server_name - - def getsockname(self): - raise NotImplementedError # pragma: no cover - - def accept_connections(self): - self.accepting = True - self.socket.listen(self.adj.backlog) # Get around asyncore NT limit - - def add_task(self, task): - self.task_dispatcher.add_task(task) - - def readable(self): - now = time.time() - if now >= self.next_channel_cleanup: - self.next_channel_cleanup = now + self.adj.cleanup_interval - self.maintenance(now) - return (self.accepting and len(self._map) < self.adj.connection_limit) - - def writable(self): - return False - - def handle_read(self): - pass - - def handle_connect(self): - pass - - def handle_accept(self): - try: - v = self.accept() - if v is None: - return - conn, addr = v - except socket.error: - # Linux: On rare occasions we get a bogus socket back from - # accept. socketmodule.c:makesockaddr complains that the - # address family is unknown. We don't want the whole server - # to shut down because of this. - if self.adj.log_socket_errors: - self.logger.warning('server accept() threw an exception', - exc_info=True) - return - self.set_socket_options(conn) - addr = self.fix_addr(addr) - self.channel_class(self, conn, addr, self.adj, map=self._map) - - def run(self): - try: - self.asyncore.loop( - timeout=self.adj.asyncore_loop_timeout, - map=self._map, - use_poll=self.adj.asyncore_use_poll, - ) - except (SystemExit, KeyboardInterrupt): - self.task_dispatcher.shutdown() - - def pull_trigger(self): - self.trigger.pull_trigger() - - def set_socket_options(self, conn): - pass - - def fix_addr(self, addr): - return addr - - def maintenance(self, now): - """ - Closes channels that have not had any activity in a while. - - The timeout is configured through adj.channel_timeout (seconds). - """ - cutoff = now - self.adj.channel_timeout - for channel in self.active_channels.values(): - if (not channel.requests) and channel.last_activity < cutoff: - channel.will_close = True - - def print_listen(self, format_str): # pragma: nocover - print(format_str.format(self.effective_host, self.effective_port)) - - -class TcpWSGIServer(BaseWSGIServer): - - def bind_server_socket(self): - (_, _, _, sockaddr) = self.sockinfo - self.bind(sockaddr) - - def getsockname(self): - try: - return self.socketmod.getnameinfo( - self.socket.getsockname(), - self.socketmod.NI_NUMERICSERV - ) - except: # pragma: no cover - # This only happens on Linux because a DNS issue is considered a - # temporary failure that will raise (even when NI_NAMEREQD is not - # set). Instead we try again, but this time we just ask for the - # numerichost and the numericserv (port) and return those. It is - # better than nothing. - return self.socketmod.getnameinfo( - self.socket.getsockname(), - self.socketmod.NI_NUMERICHOST | self.socketmod.NI_NUMERICSERV - ) - - def set_socket_options(self, conn): - for (level, optname, value) in self.adj.socket_options: - conn.setsockopt(level, optname, value) - - -if hasattr(socket, 'AF_UNIX'): - - class UnixWSGIServer(BaseWSGIServer): - - def __init__(self, - application, - map=None, - _start=True, # test shim - _sock=None, # test shim - dispatcher=None, # dispatcher - adj=None, # adjustments - sockinfo=None, # opaque object - **kw): - if sockinfo is None: - sockinfo = (socket.AF_UNIX, socket.SOCK_STREAM, None, None) - - super(UnixWSGIServer, self).__init__( - application, - map=map, - _start=_start, - _sock=_sock, - dispatcher=dispatcher, - adj=adj, - sockinfo=sockinfo, - **kw) - - def bind_server_socket(self): - cleanup_unix_socket(self.adj.unix_socket) - self.bind(self.adj.unix_socket) - if os.path.exists(self.adj.unix_socket): - os.chmod(self.adj.unix_socket, self.adj.unix_socket_perms) - - def getsockname(self): - return ('unix', self.socket.getsockname()) - - def fix_addr(self, addr): - return ('localhost', None) - -# Compatibility alias. -WSGIServer = TcpWSGIServer diff --git a/libs/waitress/task.py b/libs/waitress/task.py deleted file mode 100644 index 4ce410cf1..000000000 --- a/libs/waitress/task.py +++ /dev/null @@ -1,528 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001, 2002 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## - -import socket -import sys -import threading -import time - -from waitress.buffers import ReadOnlyFileBasedBuffer - -from waitress.compat import ( - tobytes, - Queue, - Empty, - reraise, -) - -from waitress.utilities import ( - build_http_date, - logger, -) - -rename_headers = { # or keep them without the HTTP_ prefix added - 'CONTENT_LENGTH': 'CONTENT_LENGTH', - 'CONTENT_TYPE': 'CONTENT_TYPE', -} - -hop_by_hop = frozenset(( - 'connection', - 'keep-alive', - 'proxy-authenticate', - 'proxy-authorization', - 'te', - 'trailers', - 'transfer-encoding', - 'upgrade' -)) - -class JustTesting(Exception): - pass - -class ThreadedTaskDispatcher(object): - """A Task Dispatcher that creates a thread for each task. - """ - stop_count = 0 # Number of threads that will stop soon. - logger = logger - - def __init__(self): - self.threads = {} # { thread number -> 1 } - self.queue = Queue() - self.thread_mgmt_lock = threading.Lock() - - def start_new_thread(self, target, args): - t = threading.Thread(target=target, name='waitress', args=args) - t.daemon = True - t.start() - - def handler_thread(self, thread_no): - threads = self.threads - try: - while threads.get(thread_no): - task = self.queue.get() - if task is None: - # Special value: kill this thread. - break - try: - task.service() - except Exception as e: - self.logger.exception( - 'Exception when servicing %r' % task) - if isinstance(e, JustTesting): - break - finally: - with self.thread_mgmt_lock: - self.stop_count -= 1 - threads.pop(thread_no, None) - - def set_thread_count(self, count): - with self.thread_mgmt_lock: - threads = self.threads - thread_no = 0 - running = len(threads) - self.stop_count - while running < count: - # Start threads. - while thread_no in threads: - thread_no = thread_no + 1 - threads[thread_no] = 1 - running += 1 - self.start_new_thread(self.handler_thread, (thread_no,)) - thread_no = thread_no + 1 - if running > count: - # Stop threads. - to_stop = running - count - self.stop_count += to_stop - for n in range(to_stop): - self.queue.put(None) - running -= 1 - - def add_task(self, task): - try: - task.defer() - self.queue.put(task) - except: - task.cancel() - raise - - def shutdown(self, cancel_pending=True, timeout=5): - self.set_thread_count(0) - # Ensure the threads shut down. - threads = self.threads - expiration = time.time() + timeout - while threads: - if time.time() >= expiration: - self.logger.warning( - "%d thread(s) still running" % - len(threads)) - break - time.sleep(0.1) - if cancel_pending: - # Cancel remaining tasks. - try: - queue = self.queue - while not queue.empty(): - task = queue.get() - if task is not None: - task.cancel() - except Empty: # pragma: no cover - pass - return True - return False - -class Task(object): - close_on_finish = False - status = '200 OK' - wrote_header = False - start_time = 0 - content_length = None - content_bytes_written = 0 - logged_write_excess = False - complete = False - chunked_response = False - logger = logger - - def __init__(self, channel, request): - self.channel = channel - self.request = request - self.response_headers = [] - version = request.version - if version not in ('1.0', '1.1'): - # fall back to a version we support. - version = '1.0' - self.version = version - - def service(self): - try: - try: - self.start() - self.execute() - self.finish() - except socket.error: - self.close_on_finish = True - if self.channel.adj.log_socket_errors: - raise - finally: - pass - - def cancel(self): - self.close_on_finish = True - - def defer(self): - pass - - def build_response_header(self): - version = self.version - # Figure out whether the connection should be closed. - connection = self.request.headers.get('CONNECTION', '').lower() - response_headers = self.response_headers - content_length_header = None - date_header = None - server_header = None - connection_close_header = None - - for i, (headername, headerval) in enumerate(response_headers): - headername = '-'.join( - [x.capitalize() for x in headername.split('-')] - ) - if headername == 'Content-Length': - content_length_header = headerval - if headername == 'Date': - date_header = headerval - if headername == 'Server': - server_header = headerval - if headername == 'Connection': - connection_close_header = headerval.lower() - # replace with properly capitalized version - response_headers[i] = (headername, headerval) - - if content_length_header is None and self.content_length is not None: - content_length_header = str(self.content_length) - self.response_headers.append( - ('Content-Length', content_length_header) - ) - - def close_on_finish(): - if connection_close_header is None: - response_headers.append(('Connection', 'close')) - self.close_on_finish = True - - if version == '1.0': - if connection == 'keep-alive': - if not content_length_header: - close_on_finish() - else: - response_headers.append(('Connection', 'Keep-Alive')) - else: - close_on_finish() - - elif version == '1.1': - if connection == 'close': - close_on_finish() - - if not content_length_header: - response_headers.append(('Transfer-Encoding', 'chunked')) - self.chunked_response = True - if not self.close_on_finish: - close_on_finish() - - # under HTTP 1.1 keep-alive is default, no need to set the header - else: - raise AssertionError('neither HTTP/1.0 or HTTP/1.1') - - # Set the Server and Date field, if not yet specified. This is needed - # if the server is used as a proxy. - ident = self.channel.server.adj.ident - if not server_header: - response_headers.append(('Server', ident)) - else: - response_headers.append(('Via', ident)) - if not date_header: - response_headers.append(('Date', build_http_date(self.start_time))) - - first_line = 'HTTP/%s %s' % (self.version, self.status) - # NB: sorting headers needs to preserve same-named-header order - # as per RFC 2616 section 4.2; thus the key=lambda x: x[0] here; - # rely on stable sort to keep relative position of same-named headers - next_lines = ['%s: %s' % hv for hv in sorted( - self.response_headers, key=lambda x: x[0])] - lines = [first_line] + next_lines - res = '%s\r\n\r\n' % '\r\n'.join(lines) - return tobytes(res) - - def remove_content_length_header(self): - for i, (header_name, header_value) in enumerate(self.response_headers): - if header_name.lower() == 'content-length': - del self.response_headers[i] - - def start(self): - self.start_time = time.time() - - def finish(self): - if not self.wrote_header: - self.write(b'') - if self.chunked_response: - # not self.write, it will chunk it! - self.channel.write_soon(b'0\r\n\r\n') - - def write(self, data): - if not self.complete: - raise RuntimeError('start_response was not called before body ' - 'written') - channel = self.channel - if not self.wrote_header: - rh = self.build_response_header() - channel.write_soon(rh) - self.wrote_header = True - if data: - towrite = data - cl = self.content_length - if self.chunked_response: - # use chunked encoding response - towrite = tobytes(hex(len(data))[2:].upper()) + b'\r\n' - towrite += data + b'\r\n' - elif cl is not None: - towrite = data[:cl - self.content_bytes_written] - self.content_bytes_written += len(towrite) - if towrite != data and not self.logged_write_excess: - self.logger.warning( - 'application-written content exceeded the number of ' - 'bytes specified by Content-Length header (%s)' % cl) - self.logged_write_excess = True - if towrite: - channel.write_soon(towrite) - -class ErrorTask(Task): - """ An error task produces an error response - """ - complete = True - - def execute(self): - e = self.request.error - body = '%s\r\n\r\n%s' % (e.reason, e.body) - tag = '\r\n\r\n(generated by waitress)' - body = body + tag - self.status = '%s %s' % (e.code, e.reason) - cl = len(body) - self.content_length = cl - self.response_headers.append(('Content-Length', str(cl))) - self.response_headers.append(('Content-Type', 'text/plain')) - if self.version == '1.1': - connection = self.request.headers.get('CONNECTION', '').lower() - if connection == 'close': - self.response_headers.append(('Connection', 'close')) - # under HTTP 1.1 keep-alive is default, no need to set the header - else: - # HTTP 1.0 - self.response_headers.append(('Connection', 'close')) - self.close_on_finish = True - self.write(tobytes(body)) - -class WSGITask(Task): - """A WSGI task produces a response from a WSGI application. - """ - environ = None - - def execute(self): - env = self.get_environment() - - def start_response(status, headers, exc_info=None): - if self.complete and not exc_info: - raise AssertionError("start_response called a second time " - "without providing exc_info.") - if exc_info: - try: - if self.wrote_header: - # higher levels will catch and handle raised exception: - # 1. "service" method in task.py - # 2. "service" method in channel.py - # 3. "handler_thread" method in task.py - reraise(exc_info[0], exc_info[1], exc_info[2]) - else: - # As per WSGI spec existing headers must be cleared - self.response_headers = [] - finally: - exc_info = None - - self.complete = True - - if not status.__class__ is str: - raise AssertionError('status %s is not a string' % status) - if '\n' in status or '\r' in status: - raise ValueError("carriage return/line " - "feed character present in status") - - self.status = status - - # Prepare the headers for output - for k, v in headers: - if not k.__class__ is str: - raise AssertionError( - 'Header name %r is not a string in %r' % (k, (k, v)) - ) - if not v.__class__ is str: - raise AssertionError( - 'Header value %r is not a string in %r' % (v, (k, v)) - ) - - if '\n' in v or '\r' in v: - raise ValueError("carriage return/line " - "feed character present in header value") - if '\n' in k or '\r' in k: - raise ValueError("carriage return/line " - "feed character present in header name") - - kl = k.lower() - if kl == 'content-length': - self.content_length = int(v) - elif kl in hop_by_hop: - raise AssertionError( - '%s is a "hop-by-hop" header; it cannot be used by ' - 'a WSGI application (see PEP 3333)' % k) - - self.response_headers.extend(headers) - - # Return a method used to write the response data. - return self.write - - # Call the application to handle the request and write a response - app_iter = self.channel.server.application(env, start_response) - - if app_iter.__class__ is ReadOnlyFileBasedBuffer: - # NB: do not put this inside the below try: finally: which closes - # the app_iter; we need to defer closing the underlying file. It's - # intention that we don't want to call ``close`` here if the - # app_iter is a ROFBB; the buffer (and therefore the file) will - # eventually be closed within channel.py's _flush_some or - # handle_close instead. - cl = self.content_length - size = app_iter.prepare(cl) - if size: - if cl != size: - if cl is not None: - self.remove_content_length_header() - self.content_length = size - self.write(b'') # generate headers - self.channel.write_soon(app_iter) - return - - try: - first_chunk_len = None - for chunk in app_iter: - if first_chunk_len is None: - first_chunk_len = len(chunk) - # Set a Content-Length header if one is not supplied. - # start_response may not have been called until first - # iteration as per PEP, so we must reinterrogate - # self.content_length here - if self.content_length is None: - app_iter_len = None - if hasattr(app_iter, '__len__'): - app_iter_len = len(app_iter) - if app_iter_len == 1: - self.content_length = first_chunk_len - # transmit headers only after first iteration of the iterable - # that returns a non-empty bytestring (PEP 3333) - if chunk: - self.write(chunk) - - cl = self.content_length - if cl is not None: - if self.content_bytes_written != cl: - # close the connection so the client isn't sitting around - # waiting for more data when there are too few bytes - # to service content-length - self.close_on_finish = True - if self.request.command != 'HEAD': - self.logger.warning( - 'application returned too few bytes (%s) ' - 'for specified Content-Length (%s) via app_iter' % ( - self.content_bytes_written, cl), - ) - finally: - if hasattr(app_iter, 'close'): - app_iter.close() - - def get_environment(self): - """Returns a WSGI environment.""" - environ = self.environ - if environ is not None: - # Return the cached copy. - return environ - - request = self.request - path = request.path - channel = self.channel - server = channel.server - url_prefix = server.adj.url_prefix - - if path.startswith('/'): - # strip extra slashes at the beginning of a path that starts - # with any number of slashes - path = '/' + path.lstrip('/') - - if url_prefix: - # NB: url_prefix is guaranteed by the configuration machinery to - # be either the empty string or a string that starts with a single - # slash and ends without any slashes - if path == url_prefix: - # if the path is the same as the url prefix, the SCRIPT_NAME - # should be the url_prefix and PATH_INFO should be empty - path = '' - else: - # if the path starts with the url prefix plus a slash, - # the SCRIPT_NAME should be the url_prefix and PATH_INFO should - # the value of path from the slash until its end - url_prefix_with_trailing_slash = url_prefix + '/' - if path.startswith(url_prefix_with_trailing_slash): - path = path[len(url_prefix):] - - environ = {} - environ['REQUEST_METHOD'] = request.command.upper() - environ['SERVER_PORT'] = str(server.effective_port) - environ['SERVER_NAME'] = server.server_name - environ['SERVER_SOFTWARE'] = server.adj.ident - environ['SERVER_PROTOCOL'] = 'HTTP/%s' % self.version - environ['SCRIPT_NAME'] = url_prefix - environ['PATH_INFO'] = path - environ['QUERY_STRING'] = request.query - host = environ['REMOTE_ADDR'] = channel.addr[0] - - headers = dict(request.headers) - if host == server.adj.trusted_proxy: - wsgi_url_scheme = headers.pop('X_FORWARDED_PROTO', - request.url_scheme) - else: - wsgi_url_scheme = request.url_scheme - if wsgi_url_scheme not in ('http', 'https'): - raise ValueError('Invalid X_FORWARDED_PROTO value') - for key, value in headers.items(): - value = value.strip() - mykey = rename_headers.get(key, None) - if mykey is None: - mykey = 'HTTP_%s' % key - if mykey not in environ: - environ[mykey] = value - - # the following environment variables are required by the WSGI spec - environ['wsgi.version'] = (1, 0) - environ['wsgi.url_scheme'] = wsgi_url_scheme - environ['wsgi.errors'] = sys.stderr # apps should use the logging module - environ['wsgi.multithread'] = True - environ['wsgi.multiprocess'] = False - environ['wsgi.run_once'] = False - environ['wsgi.input'] = request.get_body_stream() - environ['wsgi.file_wrapper'] = ReadOnlyFileBasedBuffer - - self.environ = environ - return environ diff --git a/libs/waitress/tests/__init__.py b/libs/waitress/tests/__init__.py deleted file mode 100644 index b711d3609..000000000 --- a/libs/waitress/tests/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# -# This file is necessary to make this directory a package. diff --git a/libs/waitress/tests/fixtureapps/__init__.py b/libs/waitress/tests/fixtureapps/__init__.py deleted file mode 100644 index f215a2b90..000000000 --- a/libs/waitress/tests/fixtureapps/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# package (for -m) diff --git a/libs/waitress/tests/fixtureapps/badcl.py b/libs/waitress/tests/fixtureapps/badcl.py deleted file mode 100644 index 2289a1257..000000000 --- a/libs/waitress/tests/fixtureapps/badcl.py +++ /dev/null @@ -1,12 +0,0 @@ -def app(environ, start_response): # pragma: no cover - body = b'abcdefghi' - cl = len(body) - if environ['PATH_INFO'] == '/short_body': - cl = len(body) + 1 - if environ['PATH_INFO'] == '/long_body': - cl = len(body) - 1 - start_response( - '200 OK', - [('Content-Length', str(cl)), ('Content-Type', 'text/plain')] - ) - return [body] diff --git a/libs/waitress/tests/fixtureapps/echo.py b/libs/waitress/tests/fixtureapps/echo.py deleted file mode 100644 index f5fd5d132..000000000 --- a/libs/waitress/tests/fixtureapps/echo.py +++ /dev/null @@ -1,11 +0,0 @@ -def app(environ, start_response): # pragma: no cover - cl = environ.get('CONTENT_LENGTH', None) - if cl is not None: - cl = int(cl) - body = environ['wsgi.input'].read(cl) - cl = str(len(body)) - start_response( - '200 OK', - [('Content-Length', cl), ('Content-Type', 'text/plain')] - ) - return [body] diff --git a/libs/waitress/tests/fixtureapps/error.py b/libs/waitress/tests/fixtureapps/error.py deleted file mode 100644 index cab8ad6eb..000000000 --- a/libs/waitress/tests/fixtureapps/error.py +++ /dev/null @@ -1,20 +0,0 @@ -def app(environ, start_response): # pragma: no cover - cl = environ.get('CONTENT_LENGTH', None) - if cl is not None: - cl = int(cl) - body = environ['wsgi.input'].read(cl) - cl = str(len(body)) - if environ['PATH_INFO'] == '/before_start_response': - raise ValueError('wrong') - write = start_response( - '200 OK', - [('Content-Length', cl), ('Content-Type', 'text/plain')] - ) - if environ['PATH_INFO'] == '/after_write_cb': - write('abc') - if environ['PATH_INFO'] == '/in_generator': - def foo(): - yield 'abc' - raise ValueError - return foo() - raise ValueError('wrong') diff --git a/libs/waitress/tests/fixtureapps/filewrapper.py b/libs/waitress/tests/fixtureapps/filewrapper.py deleted file mode 100644 index be35b0251..000000000 --- a/libs/waitress/tests/fixtureapps/filewrapper.py +++ /dev/null @@ -1,70 +0,0 @@ -import os - -here = os.path.dirname(os.path.abspath(__file__)) -fn = os.path.join(here, 'groundhog1.jpg') - -class KindaFilelike(object): # pragma: no cover - - def __init__(self, bytes): - self.bytes = bytes - - def read(self, n): - bytes = self.bytes[:n] - self.bytes = self.bytes[n:] - return bytes - -def app(environ, start_response): # pragma: no cover - path_info = environ['PATH_INFO'] - if path_info.startswith('/filelike'): - f = open(fn, 'rb') - f.seek(0, 2) - cl = f.tell() - f.seek(0) - if path_info == '/filelike': - headers = [ - ('Content-Length', str(cl)), - ('Content-Type', 'image/jpeg'), - ] - elif path_info == '/filelike_nocl': - headers = [('Content-Type', 'image/jpeg')] - elif path_info == '/filelike_shortcl': - # short content length - headers = [ - ('Content-Length', '1'), - ('Content-Type', 'image/jpeg'), - ] - else: - # long content length (/filelike_longcl) - headers = [ - ('Content-Length', str(cl + 10)), - ('Content-Type', 'image/jpeg'), - ] - else: - data = open(fn, 'rb').read() - cl = len(data) - f = KindaFilelike(data) - if path_info == '/notfilelike': - headers = [ - ('Content-Length', str(len(data))), - ('Content-Type', 'image/jpeg'), - ] - elif path_info == '/notfilelike_nocl': - headers = [('Content-Type', 'image/jpeg')] - elif path_info == '/notfilelike_shortcl': - # short content length - headers = [ - ('Content-Length', '1'), - ('Content-Type', 'image/jpeg'), - ] - else: - # long content length (/notfilelike_longcl) - headers = [ - ('Content-Length', str(cl + 10)), - ('Content-Type', 'image/jpeg'), - ] - - start_response( - '200 OK', - headers - ) - return environ['wsgi.file_wrapper'](f, 8192) diff --git a/libs/waitress/tests/fixtureapps/getline.py b/libs/waitress/tests/fixtureapps/getline.py deleted file mode 100644 index 7d8ae5d23..000000000 --- a/libs/waitress/tests/fixtureapps/getline.py +++ /dev/null @@ -1,17 +0,0 @@ -import sys - -if __name__ == '__main__': - try: - from urllib.request import urlopen, URLError - except ImportError: - from urllib2 import urlopen, URLError - - url = sys.argv[1] - headers = {'Content-Type': 'text/plain; charset=utf-8'} - try: - resp = urlopen(url) - line = resp.readline().decode('ascii') # py3 - except URLError: - line = 'failed to read %s' % url - sys.stdout.write(line) - sys.stdout.flush() diff --git a/libs/waitress/tests/fixtureapps/groundhog1.jpg b/libs/waitress/tests/fixtureapps/groundhog1.jpg deleted file mode 100644 index 90f610ea0..000000000 Binary files a/libs/waitress/tests/fixtureapps/groundhog1.jpg and /dev/null differ diff --git a/libs/waitress/tests/fixtureapps/nocl.py b/libs/waitress/tests/fixtureapps/nocl.py deleted file mode 100644 index 05e1d18fb..000000000 --- a/libs/waitress/tests/fixtureapps/nocl.py +++ /dev/null @@ -1,24 +0,0 @@ -def chunks(l, n): # pragma: no cover - """ Yield successive n-sized chunks from l. - """ - for i in range(0, len(l), n): - yield l[i:i + n] - -def gen(body): # pragma: no cover - for chunk in chunks(body, 10): - yield chunk - -def app(environ, start_response): # pragma: no cover - cl = environ.get('CONTENT_LENGTH', None) - if cl is not None: - cl = int(cl) - body = environ['wsgi.input'].read(cl) - start_response( - '200 OK', - [('Content-Type', 'text/plain')] - ) - if environ['PATH_INFO'] == '/list': - return [body] - if environ['PATH_INFO'] == '/list_lentwo': - return [body[0:1], body[1:]] - return gen(body) diff --git a/libs/waitress/tests/fixtureapps/runner.py b/libs/waitress/tests/fixtureapps/runner.py deleted file mode 100644 index eee0e45f9..000000000 --- a/libs/waitress/tests/fixtureapps/runner.py +++ /dev/null @@ -1,5 +0,0 @@ -def app(): # pragma: no cover - return None - -def returns_app(): # pragma: no cover - return app diff --git a/libs/waitress/tests/fixtureapps/sleepy.py b/libs/waitress/tests/fixtureapps/sleepy.py deleted file mode 100644 index 03bd0ab06..000000000 --- a/libs/waitress/tests/fixtureapps/sleepy.py +++ /dev/null @@ -1,14 +0,0 @@ -import time - -def app(environ, start_response): # pragma: no cover - if environ['PATH_INFO'] == '/sleepy': - time.sleep(2) - body = b'sleepy returned' - else: - body = b'notsleepy returned' - cl = str(len(body)) - start_response( - '200 OK', - [('Content-Length', cl), ('Content-Type', 'text/plain')] - ) - return [body] diff --git a/libs/waitress/tests/fixtureapps/toolarge.py b/libs/waitress/tests/fixtureapps/toolarge.py deleted file mode 100644 index 150e90875..000000000 --- a/libs/waitress/tests/fixtureapps/toolarge.py +++ /dev/null @@ -1,8 +0,0 @@ -def app(environ, start_response): # pragma: no cover - body = b'abcdef' - cl = len(body) - start_response( - '200 OK', - [('Content-Length', str(cl)), ('Content-Type', 'text/plain')] - ) - return [body] diff --git a/libs/waitress/tests/fixtureapps/writecb.py b/libs/waitress/tests/fixtureapps/writecb.py deleted file mode 100644 index ac59eb96f..000000000 --- a/libs/waitress/tests/fixtureapps/writecb.py +++ /dev/null @@ -1,14 +0,0 @@ -def app(environ, start_response): # pragma: no cover - path_info = environ['PATH_INFO'] - if path_info == '/no_content_length': - headers = [] - else: - headers = [('Content-Length', '9')] - write = start_response('200 OK', headers) - if path_info == '/long_body': - write(b'abcdefghij') - elif path_info == '/short_body': - write(b'abcdefgh') - else: - write(b'abcdefghi') - return [] diff --git a/libs/waitress/tests/test_adjustments.py b/libs/waitress/tests/test_adjustments.py deleted file mode 100644 index 9446705d2..000000000 --- a/libs/waitress/tests/test_adjustments.py +++ /dev/null @@ -1,294 +0,0 @@ -import sys -import socket - -from waitress.compat import ( - PY2, - WIN, - ) - -if sys.version_info[:2] == (2, 6): # pragma: no cover - import unittest2 as unittest -else: # pragma: no cover - import unittest - -class Test_asbool(unittest.TestCase): - - def _callFUT(self, s): - from waitress.adjustments import asbool - return asbool(s) - - def test_s_is_None(self): - result = self._callFUT(None) - self.assertEqual(result, False) - - def test_s_is_True(self): - result = self._callFUT(True) - self.assertEqual(result, True) - - def test_s_is_False(self): - result = self._callFUT(False) - self.assertEqual(result, False) - - def test_s_is_true(self): - result = self._callFUT('True') - self.assertEqual(result, True) - - def test_s_is_false(self): - result = self._callFUT('False') - self.assertEqual(result, False) - - def test_s_is_yes(self): - result = self._callFUT('yes') - self.assertEqual(result, True) - - def test_s_is_on(self): - result = self._callFUT('on') - self.assertEqual(result, True) - - def test_s_is_1(self): - result = self._callFUT(1) - self.assertEqual(result, True) - -class TestAdjustments(unittest.TestCase): - - def _hasIPv6(self): # pragma: nocover - if not socket.has_ipv6: - return False - - try: - socket.getaddrinfo( - '::1', - 0, - socket.AF_UNSPEC, - socket.SOCK_STREAM, - socket.IPPROTO_TCP, - socket.AI_PASSIVE | socket.AI_ADDRCONFIG - ) - - return True - except socket.gaierror as e: - # Check to see what the error is - if e.errno == socket.EAI_ADDRFAMILY: - return False - else: - raise e - - def _makeOne(self, **kw): - from waitress.adjustments import Adjustments - return Adjustments(**kw) - - def test_goodvars(self): - inst = self._makeOne( - host='localhost', - port='8080', - threads='5', - trusted_proxy='192.168.1.1', - url_scheme='https', - backlog='20', - recv_bytes='200', - send_bytes='300', - outbuf_overflow='400', - inbuf_overflow='500', - connection_limit='1000', - cleanup_interval='1100', - channel_timeout='1200', - log_socket_errors='true', - max_request_header_size='1300', - max_request_body_size='1400', - expose_tracebacks='true', - ident='abc', - asyncore_loop_timeout='5', - asyncore_use_poll=True, - unix_socket='/tmp/waitress.sock', - unix_socket_perms='777', - url_prefix='///foo/', - ipv4=True, - ipv6=False, - ) - - self.assertEqual(inst.host, 'localhost') - self.assertEqual(inst.port, 8080) - self.assertEqual(inst.threads, 5) - self.assertEqual(inst.trusted_proxy, '192.168.1.1') - self.assertEqual(inst.url_scheme, 'https') - self.assertEqual(inst.backlog, 20) - self.assertEqual(inst.recv_bytes, 200) - self.assertEqual(inst.send_bytes, 300) - self.assertEqual(inst.outbuf_overflow, 400) - self.assertEqual(inst.inbuf_overflow, 500) - self.assertEqual(inst.connection_limit, 1000) - self.assertEqual(inst.cleanup_interval, 1100) - self.assertEqual(inst.channel_timeout, 1200) - self.assertEqual(inst.log_socket_errors, True) - self.assertEqual(inst.max_request_header_size, 1300) - self.assertEqual(inst.max_request_body_size, 1400) - self.assertEqual(inst.expose_tracebacks, True) - self.assertEqual(inst.asyncore_loop_timeout, 5) - self.assertEqual(inst.asyncore_use_poll, True) - self.assertEqual(inst.ident, 'abc') - self.assertEqual(inst.unix_socket, '/tmp/waitress.sock') - self.assertEqual(inst.unix_socket_perms, 0o777) - self.assertEqual(inst.url_prefix, '/foo') - self.assertEqual(inst.ipv4, True) - self.assertEqual(inst.ipv6, False) - - bind_pairs = [ - sockaddr[:2] - for (family, _, _, sockaddr) in inst.listen - if family == socket.AF_INET - ] - - # On Travis, somehow we start listening to two sockets when resolving - # localhost... - self.assertEqual(('127.0.0.1', 8080), bind_pairs[0]) - - def test_goodvar_listen(self): - inst = self._makeOne(listen='127.0.0.1') - - bind_pairs = [(host, port) for (_, _, _, (host, port)) in inst.listen] - - self.assertEqual(bind_pairs, [('127.0.0.1', 8080)]) - - def test_default_listen(self): - inst = self._makeOne() - - bind_pairs = [(host, port) for (_, _, _, (host, port)) in inst.listen] - - self.assertEqual(bind_pairs, [('0.0.0.0', 8080)]) - - def test_multiple_listen(self): - inst = self._makeOne(listen='127.0.0.1:9090 127.0.0.1:8080') - - bind_pairs = [sockaddr[:2] for (_, _, _, sockaddr) in inst.listen] - - self.assertEqual(bind_pairs, - [('127.0.0.1', 9090), - ('127.0.0.1', 8080)]) - - def test_wildcard_listen(self): - inst = self._makeOne(listen='*:8080') - - bind_pairs = [sockaddr[:2] for (_, _, _, sockaddr) in inst.listen] - - self.assertTrue(len(bind_pairs) >= 1) - - def test_ipv6_no_port(self): # pragma: nocover - if not self._hasIPv6(): - return - - inst = self._makeOne(listen='[::1]') - - bind_pairs = [sockaddr[:2] for (_, _, _, sockaddr) in inst.listen] - - self.assertEqual(bind_pairs, [('::1', 8080)]) - - def test_bad_port(self): - self.assertRaises(ValueError, self._makeOne, listen='127.0.0.1:test') - - def test_service_port(self): - if WIN and PY2: # pragma: no cover - # On Windows and Python 2 this is broken, so we raise a ValueError - self.assertRaises( - ValueError, - self._makeOne, - listen='127.0.0.1:http', - ) - return - - inst = self._makeOne(listen='127.0.0.1:http 0.0.0.0:https') - - bind_pairs = [sockaddr[:2] for (_, _, _, sockaddr) in inst.listen] - - self.assertEqual(bind_pairs, [('127.0.0.1', 80), ('0.0.0.0', 443)]) - - def test_dont_mix_host_port_listen(self): - self.assertRaises( - ValueError, - self._makeOne, - host='localhost', - port='8080', - listen='127.0.0.1:8080', - ) - - def test_badvar(self): - self.assertRaises(ValueError, self._makeOne, nope=True) - - def test_ipv4_disabled(self): - self.assertRaises(ValueError, self._makeOne, ipv4=False, listen="127.0.0.1:8080") - - def test_ipv6_disabled(self): - self.assertRaises(ValueError, self._makeOne, ipv6=False, listen="[::]:8080") - -class TestCLI(unittest.TestCase): - - def parse(self, argv): - from waitress.adjustments import Adjustments - return Adjustments.parse_args(argv) - - def test_noargs(self): - opts, args = self.parse([]) - self.assertDictEqual(opts, {'call': False, 'help': False}) - self.assertSequenceEqual(args, []) - - def test_help(self): - opts, args = self.parse(['--help']) - self.assertDictEqual(opts, {'call': False, 'help': True}) - self.assertSequenceEqual(args, []) - - def test_call(self): - opts, args = self.parse(['--call']) - self.assertDictEqual(opts, {'call': True, 'help': False}) - self.assertSequenceEqual(args, []) - - def test_both(self): - opts, args = self.parse(['--call', '--help']) - self.assertDictEqual(opts, {'call': True, 'help': True}) - self.assertSequenceEqual(args, []) - - def test_positive_boolean(self): - opts, args = self.parse(['--expose-tracebacks']) - self.assertDictContainsSubset({'expose_tracebacks': 'true'}, opts) - self.assertSequenceEqual(args, []) - - def test_negative_boolean(self): - opts, args = self.parse(['--no-expose-tracebacks']) - self.assertDictContainsSubset({'expose_tracebacks': 'false'}, opts) - self.assertSequenceEqual(args, []) - - def test_cast_params(self): - opts, args = self.parse([ - '--host=localhost', - '--port=80', - '--unix-socket-perms=777' - ]) - self.assertDictContainsSubset({ - 'host': 'localhost', - 'port': '80', - 'unix_socket_perms': '777', - }, opts) - self.assertSequenceEqual(args, []) - - def test_listen_params(self): - opts, args = self.parse([ - '--listen=test:80', - ]) - - self.assertDictContainsSubset({ - 'listen': ' test:80' - }, opts) - self.assertSequenceEqual(args, []) - - def test_multiple_listen_params(self): - opts, args = self.parse([ - '--listen=test:80', - '--listen=test:8080', - ]) - - self.assertDictContainsSubset({ - 'listen': ' test:80 test:8080' - }, opts) - self.assertSequenceEqual(args, []) - - def test_bad_param(self): - import getopt - self.assertRaises(getopt.GetoptError, self.parse, ['--no-host']) diff --git a/libs/waitress/tests/test_buffers.py b/libs/waitress/tests/test_buffers.py deleted file mode 100644 index 46a215eb0..000000000 --- a/libs/waitress/tests/test_buffers.py +++ /dev/null @@ -1,453 +0,0 @@ -import unittest -import io - -class TestFileBasedBuffer(unittest.TestCase): - - def _makeOne(self, file=None, from_buffer=None): - from waitress.buffers import FileBasedBuffer - return FileBasedBuffer(file, from_buffer=from_buffer) - - def test_ctor_from_buffer_None(self): - inst = self._makeOne('file') - self.assertEqual(inst.file, 'file') - - def test_ctor_from_buffer(self): - from_buffer = io.BytesIO(b'data') - from_buffer.getfile = lambda *x: from_buffer - f = io.BytesIO() - inst = self._makeOne(f, from_buffer) - self.assertEqual(inst.file, f) - del from_buffer.getfile - self.assertEqual(inst.remain, 4) - from_buffer.close() - - def test___len__(self): - inst = self._makeOne() - inst.remain = 10 - self.assertEqual(len(inst), 10) - - def test___nonzero__(self): - inst = self._makeOne() - inst.remain = 10 - self.assertEqual(bool(inst), True) - inst.remain = 0 - self.assertEqual(bool(inst), True) - - def test_append(self): - f = io.BytesIO(b'data') - inst = self._makeOne(f) - inst.append(b'data2') - self.assertEqual(f.getvalue(), b'datadata2') - self.assertEqual(inst.remain, 5) - - def test_get_skip_true(self): - f = io.BytesIO(b'data') - inst = self._makeOne(f) - result = inst.get(100, skip=True) - self.assertEqual(result, b'data') - self.assertEqual(inst.remain, -4) - - def test_get_skip_false(self): - f = io.BytesIO(b'data') - inst = self._makeOne(f) - result = inst.get(100, skip=False) - self.assertEqual(result, b'data') - self.assertEqual(inst.remain, 0) - - def test_get_skip_bytes_less_than_zero(self): - f = io.BytesIO(b'data') - inst = self._makeOne(f) - result = inst.get(-1, skip=False) - self.assertEqual(result, b'data') - self.assertEqual(inst.remain, 0) - - def test_skip_remain_gt_bytes(self): - f = io.BytesIO(b'd') - inst = self._makeOne(f) - inst.remain = 1 - inst.skip(1) - self.assertEqual(inst.remain, 0) - - def test_skip_remain_lt_bytes(self): - f = io.BytesIO(b'd') - inst = self._makeOne(f) - inst.remain = 1 - self.assertRaises(ValueError, inst.skip, 2) - - def test_newfile(self): - inst = self._makeOne() - self.assertRaises(NotImplementedError, inst.newfile) - - def test_prune_remain_notzero(self): - f = io.BytesIO(b'd') - inst = self._makeOne(f) - inst.remain = 1 - nf = io.BytesIO() - inst.newfile = lambda *x: nf - inst.prune() - self.assertTrue(inst.file is not f) - self.assertEqual(nf.getvalue(), b'd') - - def test_prune_remain_zero_tell_notzero(self): - f = io.BytesIO(b'd') - inst = self._makeOne(f) - nf = io.BytesIO(b'd') - inst.newfile = lambda *x: nf - inst.remain = 0 - inst.prune() - self.assertTrue(inst.file is not f) - self.assertEqual(nf.getvalue(), b'd') - - def test_prune_remain_zero_tell_zero(self): - f = io.BytesIO() - inst = self._makeOne(f) - inst.remain = 0 - inst.prune() - self.assertTrue(inst.file is f) - - def test_close(self): - f = io.BytesIO() - inst = self._makeOne(f) - inst.close() - self.assertTrue(f.closed) - -class TestTempfileBasedBuffer(unittest.TestCase): - - def _makeOne(self, from_buffer=None): - from waitress.buffers import TempfileBasedBuffer - return TempfileBasedBuffer(from_buffer=from_buffer) - - def test_newfile(self): - inst = self._makeOne() - r = inst.newfile() - self.assertTrue(hasattr(r, 'fileno')) # file - -class TestBytesIOBasedBuffer(unittest.TestCase): - - def _makeOne(self, from_buffer=None): - from waitress.buffers import BytesIOBasedBuffer - return BytesIOBasedBuffer(from_buffer=from_buffer) - - def test_ctor_from_buffer_not_None(self): - f = io.BytesIO() - f.getfile = lambda *x: f - inst = self._makeOne(f) - self.assertTrue(hasattr(inst.file, 'read')) - - def test_ctor_from_buffer_None(self): - inst = self._makeOne() - self.assertTrue(hasattr(inst.file, 'read')) - - def test_newfile(self): - inst = self._makeOne() - r = inst.newfile() - self.assertTrue(hasattr(r, 'read')) - -class TestReadOnlyFileBasedBuffer(unittest.TestCase): - - def _makeOne(self, file, block_size=8192): - from waitress.buffers import ReadOnlyFileBasedBuffer - return ReadOnlyFileBasedBuffer(file, block_size) - - def test_prepare_not_seekable(self): - f = KindaFilelike(b'abc') - inst = self._makeOne(f) - result = inst.prepare() - self.assertEqual(result, False) - self.assertEqual(inst.remain, 0) - - def test_prepare_not_seekable_closeable(self): - f = KindaFilelike(b'abc', close=1) - inst = self._makeOne(f) - result = inst.prepare() - self.assertEqual(result, False) - self.assertEqual(inst.remain, 0) - self.assertTrue(hasattr(inst, 'close')) - - def test_prepare_seekable_closeable(self): - f = Filelike(b'abc', close=1, tellresults=[0, 10]) - inst = self._makeOne(f) - result = inst.prepare() - self.assertEqual(result, 10) - self.assertEqual(inst.remain, 10) - self.assertEqual(inst.file.seeked, 0) - self.assertTrue(hasattr(inst, 'close')) - - def test_get_numbytes_neg_one(self): - f = io.BytesIO(b'abcdef') - inst = self._makeOne(f) - inst.remain = 2 - result = inst.get(-1) - self.assertEqual(result, b'ab') - self.assertEqual(inst.remain, 2) - self.assertEqual(f.tell(), 0) - - def test_get_numbytes_gt_remain(self): - f = io.BytesIO(b'abcdef') - inst = self._makeOne(f) - inst.remain = 2 - result = inst.get(3) - self.assertEqual(result, b'ab') - self.assertEqual(inst.remain, 2) - self.assertEqual(f.tell(), 0) - - def test_get_numbytes_lt_remain(self): - f = io.BytesIO(b'abcdef') - inst = self._makeOne(f) - inst.remain = 2 - result = inst.get(1) - self.assertEqual(result, b'a') - self.assertEqual(inst.remain, 2) - self.assertEqual(f.tell(), 0) - - def test_get_numbytes_gt_remain_withskip(self): - f = io.BytesIO(b'abcdef') - inst = self._makeOne(f) - inst.remain = 2 - result = inst.get(3, skip=True) - self.assertEqual(result, b'ab') - self.assertEqual(inst.remain, 0) - self.assertEqual(f.tell(), 2) - - def test_get_numbytes_lt_remain_withskip(self): - f = io.BytesIO(b'abcdef') - inst = self._makeOne(f) - inst.remain = 2 - result = inst.get(1, skip=True) - self.assertEqual(result, b'a') - self.assertEqual(inst.remain, 1) - self.assertEqual(f.tell(), 1) - - def test___iter__(self): - data = b'a' * 10000 - f = io.BytesIO(data) - inst = self._makeOne(f) - r = b'' - for val in inst: - r += val - self.assertEqual(r, data) - - def test_append(self): - inst = self._makeOne(None) - self.assertRaises(NotImplementedError, inst.append, 'a') - -class TestOverflowableBuffer(unittest.TestCase): - - def _makeOne(self, overflow=10): - from waitress.buffers import OverflowableBuffer - return OverflowableBuffer(overflow) - - def test___len__buf_is_None(self): - inst = self._makeOne() - self.assertEqual(len(inst), 0) - - def test___len__buf_is_not_None(self): - inst = self._makeOne() - inst.buf = b'abc' - self.assertEqual(len(inst), 3) - - def test___nonzero__(self): - inst = self._makeOne() - inst.buf = b'abc' - self.assertEqual(bool(inst), True) - inst.buf = b'' - self.assertEqual(bool(inst), False) - - def test___nonzero___on_int_overflow_buffer(self): - inst = self._makeOne() - - class int_overflow_buf(bytes): - def __len__(self): - # maxint + 1 - return 0x7fffffffffffffff + 1 - inst.buf = int_overflow_buf() - self.assertEqual(bool(inst), True) - inst.buf = b'' - self.assertEqual(bool(inst), False) - - def test__create_buffer_large(self): - from waitress.buffers import TempfileBasedBuffer - inst = self._makeOne() - inst.strbuf = b'x' * 11 - inst._create_buffer() - self.assertEqual(inst.buf.__class__, TempfileBasedBuffer) - self.assertEqual(inst.buf.get(100), b'x' * 11) - self.assertEqual(inst.strbuf, b'') - - def test__create_buffer_small(self): - from waitress.buffers import BytesIOBasedBuffer - inst = self._makeOne() - inst.strbuf = b'x' * 5 - inst._create_buffer() - self.assertEqual(inst.buf.__class__, BytesIOBasedBuffer) - self.assertEqual(inst.buf.get(100), b'x' * 5) - self.assertEqual(inst.strbuf, b'') - - def test_append_with_len_more_than_max_int(self): - from waitress.compat import MAXINT - inst = self._makeOne() - inst.overflowed = True - buf = DummyBuffer(length=MAXINT) - inst.buf = buf - result = inst.append(b'x') - # we don't want this to throw an OverflowError on Python 2 (see - # https://github.com/Pylons/waitress/issues/47) - self.assertEqual(result, None) - - def test_append_buf_None_not_longer_than_srtbuf_limit(self): - inst = self._makeOne() - inst.strbuf = b'x' * 5 - inst.append(b'hello') - self.assertEqual(inst.strbuf, b'xxxxxhello') - - def test_append_buf_None_longer_than_strbuf_limit(self): - inst = self._makeOne(10000) - inst.strbuf = b'x' * 8192 - inst.append(b'hello') - self.assertEqual(inst.strbuf, b'') - self.assertEqual(len(inst.buf), 8197) - - def test_append_overflow(self): - inst = self._makeOne(10) - inst.strbuf = b'x' * 8192 - inst.append(b'hello') - self.assertEqual(inst.strbuf, b'') - self.assertEqual(len(inst.buf), 8197) - - def test_append_sz_gt_overflow(self): - from waitress.buffers import BytesIOBasedBuffer - f = io.BytesIO(b'data') - inst = self._makeOne(f) - buf = BytesIOBasedBuffer() - inst.buf = buf - inst.overflow = 2 - inst.append(b'data2') - self.assertEqual(f.getvalue(), b'data') - self.assertTrue(inst.overflowed) - self.assertNotEqual(inst.buf, buf) - - def test_get_buf_None_skip_False(self): - inst = self._makeOne() - inst.strbuf = b'x' * 5 - r = inst.get(5) - self.assertEqual(r, b'xxxxx') - - def test_get_buf_None_skip_True(self): - inst = self._makeOne() - inst.strbuf = b'x' * 5 - r = inst.get(5, skip=True) - self.assertFalse(inst.buf is None) - self.assertEqual(r, b'xxxxx') - - def test_skip_buf_None(self): - inst = self._makeOne() - inst.strbuf = b'data' - inst.skip(4) - self.assertEqual(inst.strbuf, b'') - self.assertNotEqual(inst.buf, None) - - def test_skip_buf_None_allow_prune_True(self): - inst = self._makeOne() - inst.strbuf = b'data' - inst.skip(4, True) - self.assertEqual(inst.strbuf, b'') - self.assertEqual(inst.buf, None) - - def test_prune_buf_None(self): - inst = self._makeOne() - inst.prune() - self.assertEqual(inst.strbuf, b'') - - def test_prune_with_buf(self): - inst = self._makeOne() - class Buf(object): - def prune(self): - self.pruned = True - inst.buf = Buf() - inst.prune() - self.assertEqual(inst.buf.pruned, True) - - def test_prune_with_buf_overflow(self): - inst = self._makeOne() - class DummyBuffer(io.BytesIO): - def getfile(self): - return self - def prune(self): - return True - def __len__(self): - return 5 - buf = DummyBuffer(b'data') - inst.buf = buf - inst.overflowed = True - inst.overflow = 10 - inst.prune() - self.assertNotEqual(inst.buf, buf) - - def test_prune_with_buflen_more_than_max_int(self): - from waitress.compat import MAXINT - inst = self._makeOne() - inst.overflowed = True - buf = DummyBuffer(length=MAXINT+1) - inst.buf = buf - result = inst.prune() - # we don't want this to throw an OverflowError on Python 2 (see - # https://github.com/Pylons/waitress/issues/47) - self.assertEqual(result, None) - - def test_getfile_buf_None(self): - inst = self._makeOne() - f = inst.getfile() - self.assertTrue(hasattr(f, 'read')) - - def test_getfile_buf_not_None(self): - inst = self._makeOne() - buf = io.BytesIO() - buf.getfile = lambda *x: buf - inst.buf = buf - f = inst.getfile() - self.assertEqual(f, buf) - - def test_close_nobuf(self): - inst = self._makeOne() - inst.buf = None - self.assertEqual(inst.close(), None) # doesnt raise - - def test_close_withbuf(self): - class Buffer(object): - def close(self): - self.closed = True - buf = Buffer() - inst = self._makeOne() - inst.buf = buf - inst.close() - self.assertTrue(buf.closed) - -class KindaFilelike(object): - - def __init__(self, bytes, close=None, tellresults=None): - self.bytes = bytes - self.tellresults = tellresults - if close is not None: - self.close = close - -class Filelike(KindaFilelike): - - def seek(self, v, whence=0): - self.seeked = v - - def tell(self): - v = self.tellresults.pop(0) - return v - -class DummyBuffer(object): - def __init__(self, length=0): - self.length = length - - def __len__(self): - return self.length - - def append(self, s): - self.length = self.length + len(s) - - def prune(self): - pass diff --git a/libs/waitress/tests/test_channel.py b/libs/waitress/tests/test_channel.py deleted file mode 100644 index afe6e510d..000000000 --- a/libs/waitress/tests/test_channel.py +++ /dev/null @@ -1,727 +0,0 @@ -import unittest -import io - -class TestHTTPChannel(unittest.TestCase): - - def _makeOne(self, sock, addr, adj, map=None): - from waitress.channel import HTTPChannel - server = DummyServer() - return HTTPChannel(server, sock, addr, adj=adj, map=map) - - def _makeOneWithMap(self, adj=None): - if adj is None: - adj = DummyAdjustments() - sock = DummySock() - map = {} - inst = self._makeOne(sock, '127.0.0.1', adj, map=map) - inst.outbuf_lock = DummyLock() - return inst, sock, map - - def test_ctor(self): - inst, _, map = self._makeOneWithMap() - self.assertEqual(inst.addr, '127.0.0.1') - self.assertEqual(map[100], inst) - - def test_total_outbufs_len_an_outbuf_size_gt_sys_maxint(self): - from waitress.compat import MAXINT - inst, _, map = self._makeOneWithMap() - class DummyHugeBuffer(object): - def __len__(self): - return MAXINT + 1 - inst.outbufs = [DummyHugeBuffer()] - result = inst.total_outbufs_len() - # we are testing that this method does not raise an OverflowError - # (see https://github.com/Pylons/waitress/issues/47) - self.assertEqual(result, MAXINT+1) - - def test_writable_something_in_outbuf(self): - inst, sock, map = self._makeOneWithMap() - inst.outbufs[0].append(b'abc') - self.assertTrue(inst.writable()) - - def test_writable_nothing_in_outbuf(self): - inst, sock, map = self._makeOneWithMap() - self.assertFalse(inst.writable()) - - def test_writable_nothing_in_outbuf_will_close(self): - inst, sock, map = self._makeOneWithMap() - inst.will_close = True - self.assertTrue(inst.writable()) - - def test_handle_write_not_connected(self): - inst, sock, map = self._makeOneWithMap() - inst.connected = False - self.assertFalse(inst.handle_write()) - - def test_handle_write_with_requests(self): - inst, sock, map = self._makeOneWithMap() - inst.requests = True - inst.last_activity = 0 - result = inst.handle_write() - self.assertEqual(result, None) - self.assertEqual(inst.last_activity, 0) - - def test_handle_write_no_request_with_outbuf(self): - inst, sock, map = self._makeOneWithMap() - inst.requests = [] - inst.outbufs = [DummyBuffer(b'abc')] - inst.last_activity = 0 - result = inst.handle_write() - self.assertEqual(result, None) - self.assertNotEqual(inst.last_activity, 0) - self.assertEqual(sock.sent, b'abc') - - def test_handle_write_outbuf_raises_socketerror(self): - import socket - inst, sock, map = self._makeOneWithMap() - inst.requests = [] - outbuf = DummyBuffer(b'abc', socket.error) - inst.outbufs = [outbuf] - inst.last_activity = 0 - inst.logger = DummyLogger() - result = inst.handle_write() - self.assertEqual(result, None) - self.assertEqual(inst.last_activity, 0) - self.assertEqual(sock.sent, b'') - self.assertEqual(len(inst.logger.exceptions), 1) - self.assertTrue(outbuf.closed) - - def test_handle_write_outbuf_raises_othererror(self): - inst, sock, map = self._makeOneWithMap() - inst.requests = [] - outbuf = DummyBuffer(b'abc', IOError) - inst.outbufs = [outbuf] - inst.last_activity = 0 - inst.logger = DummyLogger() - result = inst.handle_write() - self.assertEqual(result, None) - self.assertEqual(inst.last_activity, 0) - self.assertEqual(sock.sent, b'') - self.assertEqual(len(inst.logger.exceptions), 1) - self.assertTrue(outbuf.closed) - - def test_handle_write_no_requests_no_outbuf_will_close(self): - inst, sock, map = self._makeOneWithMap() - inst.requests = [] - outbuf = DummyBuffer(b'') - inst.outbufs = [outbuf] - inst.will_close = True - inst.last_activity = 0 - result = inst.handle_write() - self.assertEqual(result, None) - self.assertEqual(inst.connected, False) - self.assertEqual(sock.closed, True) - self.assertEqual(inst.last_activity, 0) - self.assertTrue(outbuf.closed) - - def test_handle_write_no_requests_force_flush(self): - inst, sock, map = self._makeOneWithMap() - inst.requests = [True] - inst.outbufs = [DummyBuffer(b'abc')] - inst.will_close = False - inst.force_flush = True - inst.last_activity = 0 - result = inst.handle_write() - self.assertEqual(result, None) - self.assertEqual(inst.will_close, False) - self.assertTrue(inst.outbuf_lock.acquired) - self.assertEqual(inst.force_flush, False) - self.assertEqual(sock.sent, b'abc') - - def test_handle_write_no_requests_outbuf_gt_send_bytes(self): - inst, sock, map = self._makeOneWithMap() - inst.requests = [True] - inst.outbufs = [DummyBuffer(b'abc')] - inst.adj.send_bytes = 2 - inst.will_close = False - inst.last_activity = 0 - result = inst.handle_write() - self.assertEqual(result, None) - self.assertEqual(inst.will_close, False) - self.assertTrue(inst.outbuf_lock.acquired) - self.assertEqual(sock.sent, b'abc') - - def test_handle_write_close_when_flushed(self): - inst, sock, map = self._makeOneWithMap() - outbuf = DummyBuffer(b'abc') - inst.outbufs = [outbuf] - inst.will_close = False - inst.close_when_flushed = True - inst.last_activity = 0 - result = inst.handle_write() - self.assertEqual(result, None) - self.assertEqual(inst.will_close, True) - self.assertEqual(inst.close_when_flushed, False) - self.assertEqual(sock.sent, b'abc') - self.assertTrue(outbuf.closed) - - def test_readable_no_requests_not_will_close(self): - inst, sock, map = self._makeOneWithMap() - inst.requests = [] - inst.will_close = False - self.assertEqual(inst.readable(), True) - - def test_readable_no_requests_will_close(self): - inst, sock, map = self._makeOneWithMap() - inst.requests = [] - inst.will_close = True - self.assertEqual(inst.readable(), False) - - def test_readable_with_requests(self): - inst, sock, map = self._makeOneWithMap() - inst.requests = True - self.assertEqual(inst.readable(), False) - - def test_handle_read_no_error(self): - inst, sock, map = self._makeOneWithMap() - inst.will_close = False - inst.recv = lambda *arg: b'abc' - inst.last_activity = 0 - L = [] - inst.received = lambda x: L.append(x) - result = inst.handle_read() - self.assertEqual(result, None) - self.assertNotEqual(inst.last_activity, 0) - self.assertEqual(L, [b'abc']) - - def test_handle_read_error(self): - import socket - inst, sock, map = self._makeOneWithMap() - inst.will_close = False - def recv(b): - raise socket.error - inst.recv = recv - inst.last_activity = 0 - inst.logger = DummyLogger() - result = inst.handle_read() - self.assertEqual(result, None) - self.assertEqual(inst.last_activity, 0) - self.assertEqual(len(inst.logger.exceptions), 1) - - def test_write_soon_empty_byte(self): - inst, sock, map = self._makeOneWithMap() - wrote = inst.write_soon(b'') - self.assertEqual(wrote, 0) - self.assertEqual(len(inst.outbufs[0]), 0) - - def test_write_soon_nonempty_byte(self): - inst, sock, map = self._makeOneWithMap() - wrote = inst.write_soon(b'a') - self.assertEqual(wrote, 1) - self.assertEqual(len(inst.outbufs[0]), 1) - - def test_write_soon_filewrapper(self): - from waitress.buffers import ReadOnlyFileBasedBuffer - f = io.BytesIO(b'abc') - wrapper = ReadOnlyFileBasedBuffer(f, 8192) - wrapper.prepare() - inst, sock, map = self._makeOneWithMap() - outbufs = inst.outbufs - orig_outbuf = outbufs[0] - wrote = inst.write_soon(wrapper) - self.assertEqual(wrote, 3) - self.assertEqual(len(outbufs), 3) - self.assertEqual(outbufs[0], orig_outbuf) - self.assertEqual(outbufs[1], wrapper) - self.assertEqual(outbufs[2].__class__.__name__, 'OverflowableBuffer') - - def test__flush_some_empty_outbuf(self): - inst, sock, map = self._makeOneWithMap() - result = inst._flush_some() - self.assertEqual(result, False) - - def test__flush_some_full_outbuf_socket_returns_nonzero(self): - inst, sock, map = self._makeOneWithMap() - inst.outbufs[0].append(b'abc') - result = inst._flush_some() - self.assertEqual(result, True) - - def test__flush_some_full_outbuf_socket_returns_zero(self): - inst, sock, map = self._makeOneWithMap() - sock.send = lambda x: False - inst.outbufs[0].append(b'abc') - result = inst._flush_some() - self.assertEqual(result, False) - - def test_flush_some_multiple_buffers_first_empty(self): - inst, sock, map = self._makeOneWithMap() - sock.send = lambda x: len(x) - buffer = DummyBuffer(b'abc') - inst.outbufs.append(buffer) - result = inst._flush_some() - self.assertEqual(result, True) - self.assertEqual(buffer.skipped, 3) - self.assertEqual(inst.outbufs, [buffer]) - - def test_flush_some_multiple_buffers_close_raises(self): - inst, sock, map = self._makeOneWithMap() - sock.send = lambda x: len(x) - buffer = DummyBuffer(b'abc') - inst.outbufs.append(buffer) - inst.logger = DummyLogger() - def doraise(): - raise NotImplementedError - inst.outbufs[0].close = doraise - result = inst._flush_some() - self.assertEqual(result, True) - self.assertEqual(buffer.skipped, 3) - self.assertEqual(inst.outbufs, [buffer]) - self.assertEqual(len(inst.logger.exceptions), 1) - - def test__flush_some_outbuf_len_gt_sys_maxint(self): - from waitress.compat import MAXINT - inst, sock, map = self._makeOneWithMap() - class DummyHugeOutbuffer(object): - def __init__(self): - self.length = MAXINT + 1 - def __len__(self): - return self.length - def get(self, numbytes): - self.length = 0 - return b'123' - def skip(self, *args): pass - buf = DummyHugeOutbuffer() - inst.outbufs = [buf] - inst.send = lambda *arg: 0 - result = inst._flush_some() - # we are testing that _flush_some doesn't raise an OverflowError - # when one of its outbufs has a __len__ that returns gt sys.maxint - self.assertEqual(result, False) - - def test_handle_close(self): - inst, sock, map = self._makeOneWithMap() - inst.handle_close() - self.assertEqual(inst.connected, False) - self.assertEqual(sock.closed, True) - - def test_handle_close_outbuf_raises_on_close(self): - inst, sock, map = self._makeOneWithMap() - def doraise(): - raise NotImplementedError - inst.outbufs[0].close = doraise - inst.logger = DummyLogger() - inst.handle_close() - self.assertEqual(inst.connected, False) - self.assertEqual(sock.closed, True) - self.assertEqual(len(inst.logger.exceptions), 1) - - def test_add_channel(self): - inst, sock, map = self._makeOneWithMap() - fileno = inst._fileno - inst.add_channel(map) - self.assertEqual(map[fileno], inst) - self.assertEqual(inst.server.active_channels[fileno], inst) - - def test_del_channel(self): - inst, sock, map = self._makeOneWithMap() - fileno = inst._fileno - inst.server.active_channels[fileno] = True - inst.del_channel(map) - self.assertEqual(map.get(fileno), None) - self.assertEqual(inst.server.active_channels.get(fileno), None) - - def test_received(self): - inst, sock, map = self._makeOneWithMap() - inst.server = DummyServer() - inst.received(b'GET / HTTP/1.1\n\n') - self.assertEqual(inst.server.tasks, [inst]) - self.assertTrue(inst.requests) - - def test_received_no_chunk(self): - inst, sock, map = self._makeOneWithMap() - self.assertEqual(inst.received(b''), False) - - def test_received_preq_not_completed(self): - inst, sock, map = self._makeOneWithMap() - inst.server = DummyServer() - preq = DummyParser() - inst.request = preq - preq.completed = False - preq.empty = True - inst.received(b'GET / HTTP/1.1\n\n') - self.assertEqual(inst.requests, ()) - self.assertEqual(inst.server.tasks, []) - - def test_received_preq_completed_empty(self): - inst, sock, map = self._makeOneWithMap() - inst.server = DummyServer() - preq = DummyParser() - inst.request = preq - preq.completed = True - preq.empty = True - inst.received(b'GET / HTTP/1.1\n\n') - self.assertEqual(inst.request, None) - self.assertEqual(inst.server.tasks, []) - - def test_received_preq_error(self): - inst, sock, map = self._makeOneWithMap() - inst.server = DummyServer() - preq = DummyParser() - inst.request = preq - preq.completed = True - preq.error = True - inst.received(b'GET / HTTP/1.1\n\n') - self.assertEqual(inst.request, None) - self.assertEqual(len(inst.server.tasks), 1) - self.assertTrue(inst.requests) - - def test_received_preq_completed_connection_close(self): - inst, sock, map = self._makeOneWithMap() - inst.server = DummyServer() - preq = DummyParser() - inst.request = preq - preq.completed = True - preq.empty = True - preq.connection_close = True - inst.received(b'GET / HTTP/1.1\n\n' + b'a' * 50000) - self.assertEqual(inst.request, None) - self.assertEqual(inst.server.tasks, []) - - def test_received_preq_completed_n_lt_data(self): - inst, sock, map = self._makeOneWithMap() - inst.server = DummyServer() - preq = DummyParser() - inst.request = preq - preq.completed = True - preq.empty = False - line = b'GET / HTTP/1.1\n\n' - preq.retval = len(line) - inst.received(line + line) - self.assertEqual(inst.request, None) - self.assertEqual(len(inst.requests), 2) - self.assertEqual(len(inst.server.tasks), 1) - - def test_received_headers_finished_expect_continue_false(self): - inst, sock, map = self._makeOneWithMap() - inst.server = DummyServer() - preq = DummyParser() - inst.request = preq - preq.expect_continue = False - preq.headers_finished = True - preq.completed = False - preq.empty = False - preq.retval = 1 - inst.received(b'GET / HTTP/1.1\n\n') - self.assertEqual(inst.request, preq) - self.assertEqual(inst.server.tasks, []) - self.assertEqual(inst.outbufs[0].get(100), b'') - - def test_received_headers_finished_expect_continue_true(self): - inst, sock, map = self._makeOneWithMap() - inst.server = DummyServer() - preq = DummyParser() - inst.request = preq - preq.expect_continue = True - preq.headers_finished = True - preq.completed = False - preq.empty = False - inst.received(b'GET / HTTP/1.1\n\n') - self.assertEqual(inst.request, preq) - self.assertEqual(inst.server.tasks, []) - self.assertEqual(sock.sent, b'HTTP/1.1 100 Continue\r\n\r\n') - self.assertEqual(inst.sent_continue, True) - self.assertEqual(preq.completed, False) - - def test_received_headers_finished_expect_continue_true_sent_true(self): - inst, sock, map = self._makeOneWithMap() - inst.server = DummyServer() - preq = DummyParser() - inst.request = preq - preq.expect_continue = True - preq.headers_finished = True - preq.completed = False - preq.empty = False - inst.sent_continue = True - inst.received(b'GET / HTTP/1.1\n\n') - self.assertEqual(inst.request, preq) - self.assertEqual(inst.server.tasks, []) - self.assertEqual(sock.sent, b'') - self.assertEqual(inst.sent_continue, True) - self.assertEqual(preq.completed, False) - - def test_service_no_requests(self): - inst, sock, map = self._makeOneWithMap() - inst.requests = [] - inst.service() - self.assertEqual(inst.requests, []) - self.assertTrue(inst.force_flush) - self.assertTrue(inst.last_activity) - - def test_service_with_one_request(self): - inst, sock, map = self._makeOneWithMap() - request = DummyRequest() - inst.task_class = DummyTaskClass() - inst.requests = [request] - inst.service() - self.assertEqual(inst.requests, []) - self.assertTrue(request.serviced) - self.assertTrue(request.closed) - - def test_service_with_one_error_request(self): - inst, sock, map = self._makeOneWithMap() - request = DummyRequest() - request.error = DummyError() - inst.error_task_class = DummyTaskClass() - inst.requests = [request] - inst.service() - self.assertEqual(inst.requests, []) - self.assertTrue(request.serviced) - self.assertTrue(request.closed) - - def test_service_with_multiple_requests(self): - inst, sock, map = self._makeOneWithMap() - request1 = DummyRequest() - request2 = DummyRequest() - inst.task_class = DummyTaskClass() - inst.requests = [request1, request2] - inst.service() - self.assertEqual(inst.requests, []) - self.assertTrue(request1.serviced) - self.assertTrue(request2.serviced) - self.assertTrue(request1.closed) - self.assertTrue(request2.closed) - - def test_service_with_request_raises(self): - inst, sock, map = self._makeOneWithMap() - inst.adj.expose_tracebacks = False - inst.server = DummyServer() - request = DummyRequest() - inst.requests = [request] - inst.task_class = DummyTaskClass(ValueError) - inst.task_class.wrote_header = False - inst.error_task_class = DummyTaskClass() - inst.logger = DummyLogger() - inst.service() - self.assertTrue(request.serviced) - self.assertEqual(inst.requests, []) - self.assertEqual(len(inst.logger.exceptions), 1) - self.assertTrue(inst.force_flush) - self.assertTrue(inst.last_activity) - self.assertFalse(inst.will_close) - self.assertEqual(inst.error_task_class.serviced, True) - self.assertTrue(request.closed) - - def test_service_with_requests_raises_already_wrote_header(self): - inst, sock, map = self._makeOneWithMap() - inst.adj.expose_tracebacks = False - inst.server = DummyServer() - request = DummyRequest() - inst.requests = [request] - inst.task_class = DummyTaskClass(ValueError) - inst.error_task_class = DummyTaskClass() - inst.logger = DummyLogger() - inst.service() - self.assertTrue(request.serviced) - self.assertEqual(inst.requests, []) - self.assertEqual(len(inst.logger.exceptions), 1) - self.assertTrue(inst.force_flush) - self.assertTrue(inst.last_activity) - self.assertTrue(inst.close_when_flushed) - self.assertEqual(inst.error_task_class.serviced, False) - self.assertTrue(request.closed) - - def test_service_with_requests_raises_didnt_write_header_expose_tbs(self): - inst, sock, map = self._makeOneWithMap() - inst.adj.expose_tracebacks = True - inst.server = DummyServer() - request = DummyRequest() - inst.requests = [request] - inst.task_class = DummyTaskClass(ValueError) - inst.task_class.wrote_header = False - inst.error_task_class = DummyTaskClass() - inst.logger = DummyLogger() - inst.service() - self.assertTrue(request.serviced) - self.assertFalse(inst.will_close) - self.assertEqual(inst.requests, []) - self.assertEqual(len(inst.logger.exceptions), 1) - self.assertTrue(inst.force_flush) - self.assertTrue(inst.last_activity) - self.assertEqual(inst.error_task_class.serviced, True) - self.assertTrue(request.closed) - - def test_service_with_requests_raises_didnt_write_header(self): - inst, sock, map = self._makeOneWithMap() - inst.adj.expose_tracebacks = False - inst.server = DummyServer() - request = DummyRequest() - inst.requests = [request] - inst.task_class = DummyTaskClass(ValueError) - inst.task_class.wrote_header = False - inst.logger = DummyLogger() - inst.service() - self.assertTrue(request.serviced) - self.assertEqual(inst.requests, []) - self.assertEqual(len(inst.logger.exceptions), 1) - self.assertTrue(inst.force_flush) - self.assertTrue(inst.last_activity) - self.assertTrue(inst.close_when_flushed) - self.assertTrue(request.closed) - - def test_cancel_no_requests(self): - inst, sock, map = self._makeOneWithMap() - inst.requests = () - inst.cancel() - self.assertEqual(inst.requests, []) - - def test_cancel_with_requests(self): - inst, sock, map = self._makeOneWithMap() - inst.requests = [None] - inst.cancel() - self.assertEqual(inst.requests, []) - - def test_defer(self): - inst, sock, map = self._makeOneWithMap() - self.assertEqual(inst.defer(), None) - -class DummySock(object): - blocking = False - closed = False - - def __init__(self): - self.sent = b'' - - def setblocking(self, *arg): - self.blocking = True - - def fileno(self): - return 100 - - def getpeername(self): - return '127.0.0.1' - - def close(self): - self.closed = True - - def send(self, data): - self.sent += data - return len(data) - -class DummyLock(object): - - def __init__(self, acquirable=True): - self.acquirable = acquirable - - def acquire(self, val): - self.val = val - self.acquired = True - return self.acquirable - - def release(self): - self.released = True - - def __exit__(self, type, val, traceback): - self.acquire(True) - - def __enter__(self): - pass - -class DummyBuffer(object): - closed = False - - def __init__(self, data, toraise=None): - self.data = data - self.toraise = toraise - - def get(self, *arg): - if self.toraise: - raise self.toraise - data = self.data - self.data = b'' - return data - - def skip(self, num, x): - self.skipped = num - - def __len__(self): - return len(self.data) - - def close(self): - self.closed = True - -class DummyAdjustments(object): - outbuf_overflow = 1048576 - inbuf_overflow = 512000 - cleanup_interval = 900 - send_bytes = 9000 - url_scheme = 'http' - channel_timeout = 300 - log_socket_errors = True - recv_bytes = 8192 - expose_tracebacks = True - ident = 'waitress' - max_request_header_size = 10000 - -class DummyServer(object): - trigger_pulled = False - adj = DummyAdjustments() - - def __init__(self): - self.tasks = [] - self.active_channels = {} - - def add_task(self, task): - self.tasks.append(task) - - def pull_trigger(self): - self.trigger_pulled = True - -class DummyParser(object): - version = 1 - data = None - completed = True - empty = False - headers_finished = False - expect_continue = False - retval = None - error = None - connection_close = False - - def received(self, data): - self.data = data - if self.retval is not None: - return self.retval - return len(data) - -class DummyRequest(object): - error = None - path = '/' - version = '1.0' - closed = False - - def __init__(self): - self.headers = {} - - def close(self): - self.closed = True - -class DummyLogger(object): - - def __init__(self): - self.exceptions = [] - - def exception(self, msg): - self.exceptions.append(msg) - -class DummyError(object): - code = '431' - reason = 'Bleh' - body = 'My body' - -class DummyTaskClass(object): - wrote_header = True - close_on_finish = False - serviced = False - - def __init__(self, toraise=None): - self.toraise = toraise - - def __call__(self, channel, request): - self.request = request - return self - - def service(self): - self.serviced = True - self.request.serviced = True - if self.toraise: - raise self.toraise diff --git a/libs/waitress/tests/test_compat.py b/libs/waitress/tests/test_compat.py deleted file mode 100644 index b5f662572..000000000 --- a/libs/waitress/tests/test_compat.py +++ /dev/null @@ -1,20 +0,0 @@ -# -*- coding: utf-8 -*- - -import unittest - -class Test_unquote_bytes_to_wsgi(unittest.TestCase): - - def _callFUT(self, v): - from waitress.compat import unquote_bytes_to_wsgi - return unquote_bytes_to_wsgi(v) - - def test_highorder(self): - from waitress.compat import PY3 - val = b'/a%C5%9B' - result = self._callFUT(val) - if PY3: # pragma: no cover - # PEP 3333 urlunquoted-latin1-decoded-bytes - self.assertEqual(result, '/aÃ…\x9b') - else: # pragma: no cover - # sanity - self.assertEqual(result, b'/a\xc5\x9b') diff --git a/libs/waitress/tests/test_functional.py b/libs/waitress/tests/test_functional.py deleted file mode 100644 index 59ef4e4d4..000000000 --- a/libs/waitress/tests/test_functional.py +++ /dev/null @@ -1,1551 +0,0 @@ -import errno -import logging -import multiprocessing -import os -import socket -import string -import subprocess -import sys -import time -import unittest -from waitress import server -from waitress.compat import ( - httplib, - tobytes -) -from waitress.utilities import cleanup_unix_socket - -dn = os.path.dirname -here = dn(__file__) - -class NullHandler(logging.Handler): # pragma: no cover - """A logging handler that swallows all emitted messages. - """ - def emit(self, record): - pass - -def start_server(app, svr, queue, **kwargs): # pragma: no cover - """Run a fixture application. - """ - logging.getLogger('waitress').addHandler(NullHandler()) - svr(app, queue, **kwargs).run() - -class FixtureTcpWSGIServer(server.TcpWSGIServer): - """A version of TcpWSGIServer that relays back what it's bound to. - """ - - family = socket.AF_INET # Testing - - def __init__(self, application, queue, **kw): # pragma: no cover - # Coverage doesn't see this as it's ran in a separate process. - kw['port'] = 0 # Bind to any available port. - super(FixtureTcpWSGIServer, self).__init__(application, **kw) - host, port = self.socket.getsockname() - if os.name == 'nt': - host = '127.0.0.1' - queue.put((host, port)) - -class SubprocessTests(object): - - # For nose: all tests may be ran in separate processes. - _multiprocess_can_split_ = True - - exe = sys.executable - - server = None - - def start_subprocess(self, target, **kw): - # Spawn a server process. - self.queue = multiprocessing.Queue() - self.proc = multiprocessing.Process( - target=start_server, - args=(target, self.server, self.queue), - kwargs=kw, - ) - self.proc.start() - if self.proc.exitcode is not None: # pragma: no cover - raise RuntimeError("%s didn't start" % str(target)) - # Get the socket the server is listening on. - self.bound_to = self.queue.get(timeout=5) - self.sock = self.create_socket() - - def stop_subprocess(self): - if self.proc.exitcode is None: - self.proc.terminate() - self.sock.close() - # This give us one FD back ... - self.queue.close() - - def assertline(self, line, status, reason, version): - v, s, r = (x.strip() for x in line.split(None, 2)) - self.assertEqual(s, tobytes(status)) - self.assertEqual(r, tobytes(reason)) - self.assertEqual(v, tobytes(version)) - - def create_socket(self): - return socket.socket(self.server.family, socket.SOCK_STREAM) - - def connect(self): - self.sock.connect(self.bound_to) - - def make_http_connection(self): - raise NotImplementedError # pragma: no cover - - def send_check_error(self, to_send): - self.sock.send(to_send) - -class TcpTests(SubprocessTests): - - server = FixtureTcpWSGIServer - - def make_http_connection(self): - return httplib.HTTPConnection(*self.bound_to) - -class SleepyThreadTests(TcpTests, unittest.TestCase): - # test that sleepy thread doesnt block other requests - - def setUp(self): - from waitress.tests.fixtureapps import sleepy - self.start_subprocess(sleepy.app) - - def tearDown(self): - self.stop_subprocess() - - def test_it(self): - getline = os.path.join(here, 'fixtureapps', 'getline.py') - cmds = ( - [self.exe, getline, 'http://%s:%d/sleepy' % self.bound_to], - [self.exe, getline, 'http://%s:%d/' % self.bound_to] - ) - r, w = os.pipe() - procs = [] - for cmd in cmds: - procs.append(subprocess.Popen(cmd, stdout=w)) - time.sleep(3) - for proc in procs: - if proc.returncode is not None: # pragma: no cover - proc.terminate() - # the notsleepy response should always be first returned (it sleeps - # for 2 seconds, then returns; the notsleepy response should be - # processed in the meantime) - result = os.read(r, 10000) - os.close(r) - os.close(w) - self.assertEqual(result, b'notsleepy returnedsleepy returned') - -class EchoTests(object): - - def setUp(self): - from waitress.tests.fixtureapps import echo - self.start_subprocess(echo.app) - - def tearDown(self): - self.stop_subprocess() - - def test_date_and_server(self): - to_send = ("GET / HTTP/1.0\n" - "Content-Length: 0\n\n") - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - self.assertEqual(headers.get('server'), 'waitress') - self.assertTrue(headers.get('date')) - - def test_bad_host_header(self): - # http://corte.si/posts/code/pathod/pythonservers/index.html - to_send = ("GET / HTTP/1.0\n" - " Host: 0\n\n") - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '400', 'Bad Request', 'HTTP/1.0') - self.assertEqual(headers.get('server'), 'waitress') - self.assertTrue(headers.get('date')) - - def test_send_with_body(self): - to_send = ("GET / HTTP/1.0\n" - "Content-Length: 5\n\n") - to_send += 'hello' - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - self.assertEqual(headers.get('content-length'), '5') - self.assertEqual(response_body, b'hello') - - def test_send_empty_body(self): - to_send = ("GET / HTTP/1.0\n" - "Content-Length: 0\n\n") - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - self.assertEqual(headers.get('content-length'), '0') - self.assertEqual(response_body, b'') - - def test_multiple_requests_with_body(self): - for x in range(3): - self.sock = self.create_socket() - self.test_send_with_body() - self.sock.close() - - def test_multiple_requests_without_body(self): - for x in range(3): - self.sock = self.create_socket() - self.test_send_empty_body() - self.sock.close() - - def test_without_crlf(self): - data = "Echo\nthis\r\nplease" - s = tobytes( - "GET / HTTP/1.0\n" - "Connection: close\n" - "Content-Length: %d\n" - "\n" - "%s" % (len(data), data) - ) - self.connect() - self.sock.send(s) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - self.assertEqual(int(headers['content-length']), len(data)) - self.assertEqual(len(response_body), len(data)) - self.assertEqual(response_body, tobytes(data)) - - def test_large_body(self): - # 1024 characters. - body = 'This string has 32 characters.\r\n' * 32 - s = tobytes( - "GET / HTTP/1.0\n" - "Content-Length: %d\n" - "\n" - "%s" % (len(body), body) - ) - self.connect() - self.sock.send(s) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - self.assertEqual(headers.get('content-length'), '1024') - self.assertEqual(response_body, tobytes(body)) - - def test_many_clients(self): - conns = [] - for n in range(50): - h = self.make_http_connection() - h.request("GET", "/", headers={"Accept": "text/plain"}) - conns.append(h) - responses = [] - for h in conns: - response = h.getresponse() - self.assertEqual(response.status, 200) - responses.append(response) - for response in responses: - response.read() - - def test_chunking_request_without_content(self): - header = tobytes( - "GET / HTTP/1.1\n" - "Transfer-Encoding: chunked\n\n" - ) - self.connect() - self.sock.send(header) - self.sock.send(b"0\r\n\r\n") - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - self.assertEqual(response_body, b'') - self.assertEqual(headers['content-length'], '0') - self.assertFalse('transfer-encoding' in headers) - - def test_chunking_request_with_content(self): - control_line = b"20;\r\n" # 20 hex = 32 dec - s = b'This string has 32 characters.\r\n' - expected = s * 12 - header = tobytes( - "GET / HTTP/1.1\n" - "Transfer-Encoding: chunked\n\n" - ) - self.connect() - self.sock.send(header) - fp = self.sock.makefile('rb', 0) - for n in range(12): - self.sock.send(control_line) - self.sock.send(s) - self.sock.send(b"0\r\n\r\n") - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - self.assertEqual(response_body, expected) - self.assertEqual(headers['content-length'], str(len(expected))) - self.assertFalse('transfer-encoding' in headers) - - def test_broken_chunked_encoding(self): - control_line = "20;\r\n" # 20 hex = 32 dec - s = 'This string has 32 characters.\r\n' - to_send = "GET / HTTP/1.1\nTransfer-Encoding: chunked\n\n" - to_send += (control_line + s) - # garbage in input - to_send += "GET / HTTP/1.1\nTransfer-Encoding: chunked\n\n" - to_send += (control_line + s) - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - # receiver caught garbage and turned it into a 400 - self.assertline(line, '400', 'Bad Request', 'HTTP/1.1') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - self.assertEqual(sorted(headers.keys()), - ['content-length', 'content-type', 'date', 'server']) - self.assertEqual(headers['content-type'], 'text/plain') - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_keepalive_http_10(self): - # Handling of Keep-Alive within HTTP 1.0 - data = "Default: Don't keep me alive" - s = tobytes( - "GET / HTTP/1.0\n" - "Content-Length: %d\n" - "\n" - "%s" % (len(data), data) - ) - self.connect() - self.sock.send(s) - response = httplib.HTTPResponse(self.sock) - response.begin() - self.assertEqual(int(response.status), 200) - connection = response.getheader('Connection', '') - # We sent no Connection: Keep-Alive header - # Connection: close (or no header) is default. - self.assertTrue(connection != 'Keep-Alive') - - def test_keepalive_http10_explicit(self): - # If header Connection: Keep-Alive is explicitly sent, - # we want to keept the connection open, we also need to return - # the corresponding header - data = "Keep me alive" - s = tobytes( - "GET / HTTP/1.0\n" - "Connection: Keep-Alive\n" - "Content-Length: %d\n" - "\n" - "%s" % (len(data), data) - ) - self.connect() - self.sock.send(s) - response = httplib.HTTPResponse(self.sock) - response.begin() - self.assertEqual(int(response.status), 200) - connection = response.getheader('Connection', '') - self.assertEqual(connection, 'Keep-Alive') - - def test_keepalive_http_11(self): - # Handling of Keep-Alive within HTTP 1.1 - - # All connections are kept alive, unless stated otherwise - data = "Default: Keep me alive" - s = tobytes( - "GET / HTTP/1.1\n" - "Content-Length: %d\n" - "\n" - "%s" % (len(data), data)) - self.connect() - self.sock.send(s) - response = httplib.HTTPResponse(self.sock) - response.begin() - self.assertEqual(int(response.status), 200) - self.assertTrue(response.getheader('connection') != 'close') - - def test_keepalive_http11_explicit(self): - # Explicitly set keep-alive - data = "Default: Keep me alive" - s = tobytes( - "GET / HTTP/1.1\n" - "Connection: keep-alive\n" - "Content-Length: %d\n" - "\n" - "%s" % (len(data), data) - ) - self.connect() - self.sock.send(s) - response = httplib.HTTPResponse(self.sock) - response.begin() - self.assertEqual(int(response.status), 200) - self.assertTrue(response.getheader('connection') != 'close') - - def test_keepalive_http11_connclose(self): - # specifying Connection: close explicitly - data = "Don't keep me alive" - s = tobytes( - "GET / HTTP/1.1\n" - "Connection: close\n" - "Content-Length: %d\n" - "\n" - "%s" % (len(data), data) - ) - self.connect() - self.sock.send(s) - response = httplib.HTTPResponse(self.sock) - response.begin() - self.assertEqual(int(response.status), 200) - self.assertEqual(response.getheader('connection'), 'close') - -class PipeliningTests(object): - - def setUp(self): - from waitress.tests.fixtureapps import echo - self.start_subprocess(echo.app) - - def tearDown(self): - self.stop_subprocess() - - def test_pipelining(self): - s = ("GET / HTTP/1.0\r\n" - "Connection: %s\r\n" - "Content-Length: %d\r\n" - "\r\n" - "%s") - to_send = b'' - count = 25 - for n in range(count): - body = "Response #%d\r\n" % (n + 1) - if n + 1 < count: - conn = 'keep-alive' - else: - conn = 'close' - to_send += tobytes(s % (conn, len(body), body)) - - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - for n in range(count): - expect_body = tobytes("Response #%d\r\n" % (n + 1)) - line = fp.readline() # status line - version, status, reason = (x.strip() for x in line.split(None, 2)) - headers = parse_headers(fp) - length = int(headers.get('content-length')) or None - response_body = fp.read(length) - self.assertEqual(int(status), 200) - self.assertEqual(length, len(response_body)) - self.assertEqual(response_body, expect_body) - -class ExpectContinueTests(object): - - def setUp(self): - from waitress.tests.fixtureapps import echo - self.start_subprocess(echo.app) - - def tearDown(self): - self.stop_subprocess() - - def test_expect_continue(self): - # specifying Connection: close explicitly - data = "I have expectations" - to_send = tobytes( - "GET / HTTP/1.1\n" - "Connection: close\n" - "Content-Length: %d\n" - "Expect: 100-continue\n" - "\n" - "%s" % (len(data), data) - ) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line = fp.readline() # continue status line - version, status, reason = (x.strip() for x in line.split(None, 2)) - self.assertEqual(int(status), 100) - self.assertEqual(reason, b'Continue') - self.assertEqual(version, b'HTTP/1.1') - fp.readline() # blank line - line = fp.readline() # next status line - version, status, reason = (x.strip() for x in line.split(None, 2)) - headers = parse_headers(fp) - length = int(headers.get('content-length')) or None - response_body = fp.read(length) - self.assertEqual(int(status), 200) - self.assertEqual(length, len(response_body)) - self.assertEqual(response_body, tobytes(data)) - -class BadContentLengthTests(object): - - def setUp(self): - from waitress.tests.fixtureapps import badcl - self.start_subprocess(badcl.app) - - def tearDown(self): - self.stop_subprocess() - - def test_short_body(self): - # check to see if server closes connection when body is too short - # for cl header - to_send = tobytes( - "GET /short_body HTTP/1.0\n" - "Connection: Keep-Alive\n" - "Content-Length: 0\n" - "\n" - ) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line = fp.readline() # status line - version, status, reason = (x.strip() for x in line.split(None, 2)) - headers = parse_headers(fp) - content_length = int(headers.get('content-length')) - response_body = fp.read(content_length) - self.assertEqual(int(status), 200) - self.assertNotEqual(content_length, len(response_body)) - self.assertEqual(len(response_body), content_length - 1) - self.assertEqual(response_body, tobytes('abcdefghi')) - # remote closed connection (despite keepalive header); not sure why - # first send succeeds - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_long_body(self): - # check server doesnt close connection when body is too short - # for cl header - to_send = tobytes( - "GET /long_body HTTP/1.0\n" - "Connection: Keep-Alive\n" - "Content-Length: 0\n" - "\n" - ) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line = fp.readline() # status line - version, status, reason = (x.strip() for x in line.split(None, 2)) - headers = parse_headers(fp) - content_length = int(headers.get('content-length')) or None - response_body = fp.read(content_length) - self.assertEqual(int(status), 200) - self.assertEqual(content_length, len(response_body)) - self.assertEqual(response_body, tobytes('abcdefgh')) - # remote does not close connection (keepalive header) - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line = fp.readline() # status line - version, status, reason = (x.strip() for x in line.split(None, 2)) - headers = parse_headers(fp) - content_length = int(headers.get('content-length')) or None - response_body = fp.read(content_length) - self.assertEqual(int(status), 200) - -class NoContentLengthTests(object): - - def setUp(self): - from waitress.tests.fixtureapps import nocl - self.start_subprocess(nocl.app) - - def tearDown(self): - self.stop_subprocess() - - def test_http10_generator(self): - body = string.ascii_letters - to_send = ("GET / HTTP/1.0\n" - "Connection: Keep-Alive\n" - "Content-Length: %d\n\n" % len(body)) - to_send += body - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - self.assertEqual(headers.get('content-length'), None) - self.assertEqual(headers.get('connection'), 'close') - self.assertEqual(response_body, tobytes(body)) - # remote closed connection (despite keepalive header), because - # generators cannot have a content-length divined - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_http10_list(self): - body = string.ascii_letters - to_send = ("GET /list HTTP/1.0\n" - "Connection: Keep-Alive\n" - "Content-Length: %d\n\n" % len(body)) - to_send += body - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - self.assertEqual(headers['content-length'], str(len(body))) - self.assertEqual(headers.get('connection'), 'Keep-Alive') - self.assertEqual(response_body, tobytes(body)) - # remote keeps connection open because it divined the content length - # from a length-1 list - self.sock.send(to_send) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - - def test_http10_listlentwo(self): - body = string.ascii_letters - to_send = ("GET /list_lentwo HTTP/1.0\n" - "Connection: Keep-Alive\n" - "Content-Length: %d\n\n" % len(body)) - to_send += body - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - self.assertEqual(headers.get('content-length'), None) - self.assertEqual(headers.get('connection'), 'close') - self.assertEqual(response_body, tobytes(body)) - # remote closed connection (despite keepalive header), because - # lists of length > 1 cannot have their content length divined - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_http11_generator(self): - body = string.ascii_letters - to_send = ("GET / HTTP/1.1\n" - "Content-Length: %s\n\n" % len(body)) - to_send += body - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb') - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - expected = b'' - for chunk in chunks(body, 10): - expected += tobytes( - '%s\r\n%s\r\n' % (str(hex(len(chunk))[2:].upper()), chunk) - ) - expected += b'0\r\n\r\n' - self.assertEqual(response_body, expected) - # connection is always closed at the end of a chunked response - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_http11_list(self): - body = string.ascii_letters - to_send = ("GET /list HTTP/1.1\n" - "Content-Length: %d\n\n" % len(body)) - to_send += body - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - self.assertEqual(headers['content-length'], str(len(body))) - self.assertEqual(response_body, tobytes(body)) - # remote keeps connection open because it divined the content length - # from a length-1 list - self.sock.send(to_send) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - - def test_http11_listlentwo(self): - body = string.ascii_letters - to_send = ("GET /list_lentwo HTTP/1.1\n" - "Content-Length: %s\n\n" % len(body)) - to_send += body - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb') - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - expected = b'' - for chunk in (body[0], body[1:]): - expected += tobytes( - '%s\r\n%s\r\n' % (str(hex(len(chunk))[2:].upper()), chunk) - ) - expected += b'0\r\n\r\n' - self.assertEqual(response_body, expected) - # connection is always closed at the end of a chunked response - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - -class WriteCallbackTests(object): - - def setUp(self): - from waitress.tests.fixtureapps import writecb - self.start_subprocess(writecb.app) - - def tearDown(self): - self.stop_subprocess() - - def test_short_body(self): - # check to see if server closes connection when body is too short - # for cl header - to_send = tobytes( - "GET /short_body HTTP/1.0\n" - "Connection: Keep-Alive\n" - "Content-Length: 0\n" - "\n" - ) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - # server trusts the content-length header (5) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - cl = int(headers['content-length']) - self.assertEqual(cl, 9) - self.assertNotEqual(cl, len(response_body)) - self.assertEqual(len(response_body), cl - 1) - self.assertEqual(response_body, tobytes('abcdefgh')) - # remote closed connection (despite keepalive header) - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_long_body(self): - # check server doesnt close connection when body is too long - # for cl header - to_send = tobytes( - "GET /long_body HTTP/1.0\n" - "Connection: Keep-Alive\n" - "Content-Length: 0\n" - "\n" - ) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - content_length = int(headers.get('content-length')) or None - self.assertEqual(content_length, 9) - self.assertEqual(content_length, len(response_body)) - self.assertEqual(response_body, tobytes('abcdefghi')) - # remote does not close connection (keepalive header) - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - - def test_equal_body(self): - # check server doesnt close connection when body is equal to - # cl header - to_send = tobytes( - "GET /equal_body HTTP/1.0\n" - "Connection: Keep-Alive\n" - "Content-Length: 0\n" - "\n" - ) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - content_length = int(headers.get('content-length')) or None - self.assertEqual(content_length, 9) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - self.assertEqual(content_length, len(response_body)) - self.assertEqual(response_body, tobytes('abcdefghi')) - # remote does not close connection (keepalive header) - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - - def test_no_content_length(self): - # wtf happens when there's no content-length - to_send = tobytes( - "GET /no_content_length HTTP/1.0\n" - "Connection: Keep-Alive\n" - "Content-Length: 0\n" - "\n" - ) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line = fp.readline() # status line - line, headers, response_body = read_http(fp) - content_length = headers.get('content-length') - self.assertEqual(content_length, None) - self.assertEqual(response_body, tobytes('abcdefghi')) - # remote closed connection (despite keepalive header) - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - -class TooLargeTests(object): - - toobig = 1050 - - def setUp(self): - from waitress.tests.fixtureapps import toolarge - self.start_subprocess(toolarge.app, - max_request_header_size=1000, - max_request_body_size=1000) - - def tearDown(self): - self.stop_subprocess() - - def test_request_body_too_large_with_wrong_cl_http10(self): - body = 'a' * self.toobig - to_send = ("GET / HTTP/1.0\n" - "Content-Length: 5\n\n") - to_send += body - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb') - # first request succeeds (content-length 5) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - # server trusts the content-length header; no pipelining, - # so request fulfilled, extra bytes are thrown away - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_request_body_too_large_with_wrong_cl_http10_keepalive(self): - body = 'a' * self.toobig - to_send = ("GET / HTTP/1.0\n" - "Content-Length: 5\n" - "Connection: Keep-Alive\n\n") - to_send += body - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb') - # first request succeeds (content-length 5) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - line, headers, response_body = read_http(fp) - self.assertline(line, '431', 'Request Header Fields Too Large', - 'HTTP/1.0') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_request_body_too_large_with_no_cl_http10(self): - body = 'a' * self.toobig - to_send = "GET / HTTP/1.0\n\n" - to_send += body - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - # extra bytes are thrown away (no pipelining), connection closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_request_body_too_large_with_no_cl_http10_keepalive(self): - body = 'a' * self.toobig - to_send = "GET / HTTP/1.0\nConnection: Keep-Alive\n\n" - to_send += body - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - # server trusts the content-length header (assumed zero) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - line, headers, response_body = read_http(fp) - # next response overruns because the extra data appears to be - # header data - self.assertline(line, '431', 'Request Header Fields Too Large', - 'HTTP/1.0') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_request_body_too_large_with_wrong_cl_http11(self): - body = 'a' * self.toobig - to_send = ("GET / HTTP/1.1\n" - "Content-Length: 5\n\n") - to_send += body - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb') - # first request succeeds (content-length 5) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - # second response is an error response - line, headers, response_body = read_http(fp) - self.assertline(line, '431', 'Request Header Fields Too Large', - 'HTTP/1.0') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_request_body_too_large_with_wrong_cl_http11_connclose(self): - body = 'a' * self.toobig - to_send = "GET / HTTP/1.1\nContent-Length: 5\nConnection: close\n\n" - to_send += body - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - # server trusts the content-length header (5) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_request_body_too_large_with_no_cl_http11(self): - body = 'a' * self.toobig - to_send = "GET / HTTP/1.1\n\n" - to_send += body - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb') - # server trusts the content-length header (assumed 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - # server assumes pipelined requests due to http/1.1, and the first - # request was assumed c-l 0 because it had no content-length header, - # so entire body looks like the header of the subsequent request - # second response is an error response - line, headers, response_body = read_http(fp) - self.assertline(line, '431', 'Request Header Fields Too Large', - 'HTTP/1.0') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_request_body_too_large_with_no_cl_http11_connclose(self): - body = 'a' * self.toobig - to_send = "GET / HTTP/1.1\nConnection: close\n\n" - to_send += body - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - # server trusts the content-length header (assumed 0) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_request_body_too_large_chunked_encoding(self): - control_line = "20;\r\n" # 20 hex = 32 dec - s = 'This string has 32 characters.\r\n' - to_send = "GET / HTTP/1.1\nTransfer-Encoding: chunked\n\n" - repeat = control_line + s - to_send += repeat * ((self.toobig // len(repeat)) + 1) - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - # body bytes counter caught a max_request_body_size overrun - self.assertline(line, '413', 'Request Entity Too Large', 'HTTP/1.1') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - self.assertEqual(headers['content-type'], 'text/plain') - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - -class InternalServerErrorTests(object): - - def setUp(self): - from waitress.tests.fixtureapps import error - self.start_subprocess(error.app, expose_tracebacks=True) - - def tearDown(self): - self.stop_subprocess() - - def test_before_start_response_http_10(self): - to_send = "GET /before_start_response HTTP/1.0\n\n" - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '500', 'Internal Server Error', 'HTTP/1.0') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - self.assertTrue(response_body.startswith(b'Internal Server Error')) - self.assertEqual(headers['connection'], 'close') - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_before_start_response_http_11(self): - to_send = "GET /before_start_response HTTP/1.1\n\n" - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '500', 'Internal Server Error', 'HTTP/1.1') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - self.assertTrue(response_body.startswith(b'Internal Server Error')) - self.assertEqual(sorted(headers.keys()), - ['content-length', 'content-type', 'date', 'server']) - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_before_start_response_http_11_close(self): - to_send = tobytes( - "GET /before_start_response HTTP/1.1\n" - "Connection: close\n\n") - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '500', 'Internal Server Error', 'HTTP/1.1') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - self.assertTrue(response_body.startswith(b'Internal Server Error')) - self.assertEqual(sorted(headers.keys()), - ['connection', 'content-length', 'content-type', 'date', - 'server']) - self.assertEqual(headers['connection'], 'close') - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_after_start_response_http10(self): - to_send = "GET /after_start_response HTTP/1.0\n\n" - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '500', 'Internal Server Error', 'HTTP/1.0') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - self.assertTrue(response_body.startswith(b'Internal Server Error')) - self.assertEqual(sorted(headers.keys()), - ['connection', 'content-length', 'content-type', 'date', - 'server']) - self.assertEqual(headers['connection'], 'close') - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_after_start_response_http11(self): - to_send = "GET /after_start_response HTTP/1.1\n\n" - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '500', 'Internal Server Error', 'HTTP/1.1') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - self.assertTrue(response_body.startswith(b'Internal Server Error')) - self.assertEqual(sorted(headers.keys()), - ['content-length', 'content-type', 'date', 'server']) - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_after_start_response_http11_close(self): - to_send = tobytes( - "GET /after_start_response HTTP/1.1\n" - "Connection: close\n\n") - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '500', 'Internal Server Error', 'HTTP/1.1') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - self.assertTrue(response_body.startswith(b'Internal Server Error')) - self.assertEqual(sorted(headers.keys()), - ['connection', 'content-length', 'content-type', 'date', - 'server']) - self.assertEqual(headers['connection'], 'close') - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_after_write_cb(self): - to_send = "GET /after_write_cb HTTP/1.1\n\n" - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - self.assertEqual(response_body, b'') - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_in_generator(self): - to_send = "GET /in_generator HTTP/1.1\n\n" - to_send = tobytes(to_send) - self.connect() - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - self.assertEqual(response_body, b'') - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - -class FileWrapperTests(object): - - def setUp(self): - from waitress.tests.fixtureapps import filewrapper - self.start_subprocess(filewrapper.app) - - def tearDown(self): - self.stop_subprocess() - - def test_filelike_http11(self): - to_send = "GET /filelike HTTP/1.1\n\n" - to_send = tobytes(to_send) - - self.connect() - - for t in range(0, 2): - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - ct = headers['content-type'] - self.assertEqual(ct, 'image/jpeg') - self.assertTrue(b'\377\330\377' in response_body) - # connection has not been closed - - def test_filelike_nocl_http11(self): - to_send = "GET /filelike_nocl HTTP/1.1\n\n" - to_send = tobytes(to_send) - - self.connect() - - for t in range(0, 2): - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - ct = headers['content-type'] - self.assertEqual(ct, 'image/jpeg') - self.assertTrue(b'\377\330\377' in response_body) - # connection has not been closed - - def test_filelike_shortcl_http11(self): - to_send = "GET /filelike_shortcl HTTP/1.1\n\n" - to_send = tobytes(to_send) - - self.connect() - - for t in range(0, 2): - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - cl = int(headers['content-length']) - self.assertEqual(cl, 1) - self.assertEqual(cl, len(response_body)) - ct = headers['content-type'] - self.assertEqual(ct, 'image/jpeg') - self.assertTrue(b'\377' in response_body) - # connection has not been closed - - def test_filelike_longcl_http11(self): - to_send = "GET /filelike_longcl HTTP/1.1\n\n" - to_send = tobytes(to_send) - - self.connect() - - for t in range(0, 2): - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - ct = headers['content-type'] - self.assertEqual(ct, 'image/jpeg') - self.assertTrue(b'\377\330\377' in response_body) - # connection has not been closed - - def test_notfilelike_http11(self): - to_send = "GET /notfilelike HTTP/1.1\n\n" - to_send = tobytes(to_send) - - self.connect() - - for t in range(0, 2): - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - ct = headers['content-type'] - self.assertEqual(ct, 'image/jpeg') - self.assertTrue(b'\377\330\377' in response_body) - # connection has not been closed - - def test_notfilelike_nocl_http11(self): - to_send = "GET /notfilelike_nocl HTTP/1.1\n\n" - to_send = tobytes(to_send) - - self.connect() - - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - ct = headers['content-type'] - self.assertEqual(ct, 'image/jpeg') - self.assertTrue(b'\377\330\377' in response_body) - # connection has been closed (no content-length) - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_notfilelike_shortcl_http11(self): - to_send = "GET /notfilelike_shortcl HTTP/1.1\n\n" - to_send = tobytes(to_send) - - self.connect() - - for t in range(0, 2): - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - cl = int(headers['content-length']) - self.assertEqual(cl, 1) - self.assertEqual(cl, len(response_body)) - ct = headers['content-type'] - self.assertEqual(ct, 'image/jpeg') - self.assertTrue(b'\377' in response_body) - # connection has not been closed - - def test_notfilelike_longcl_http11(self): - to_send = "GET /notfilelike_longcl HTTP/1.1\n\n" - to_send = tobytes(to_send) - - self.connect() - - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.1') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body) + 10) - ct = headers['content-type'] - self.assertEqual(ct, 'image/jpeg') - self.assertTrue(b'\377\330\377' in response_body) - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_filelike_http10(self): - to_send = "GET /filelike HTTP/1.0\n\n" - to_send = tobytes(to_send) - - self.connect() - - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - ct = headers['content-type'] - self.assertEqual(ct, 'image/jpeg') - self.assertTrue(b'\377\330\377' in response_body) - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_filelike_nocl_http10(self): - to_send = "GET /filelike_nocl HTTP/1.0\n\n" - to_send = tobytes(to_send) - - self.connect() - - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - ct = headers['content-type'] - self.assertEqual(ct, 'image/jpeg') - self.assertTrue(b'\377\330\377' in response_body) - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_notfilelike_http10(self): - to_send = "GET /notfilelike HTTP/1.0\n\n" - to_send = tobytes(to_send) - - self.connect() - - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - cl = int(headers['content-length']) - self.assertEqual(cl, len(response_body)) - ct = headers['content-type'] - self.assertEqual(ct, 'image/jpeg') - self.assertTrue(b'\377\330\377' in response_body) - # connection has been closed - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - - def test_notfilelike_nocl_http10(self): - to_send = "GET /notfilelike_nocl HTTP/1.0\n\n" - to_send = tobytes(to_send) - - self.connect() - - self.sock.send(to_send) - fp = self.sock.makefile('rb', 0) - line, headers, response_body = read_http(fp) - self.assertline(line, '200', 'OK', 'HTTP/1.0') - ct = headers['content-type'] - self.assertEqual(ct, 'image/jpeg') - self.assertTrue(b'\377\330\377' in response_body) - # connection has been closed (no content-length) - self.send_check_error(to_send) - self.assertRaises(ConnectionClosed, read_http, fp) - -class TcpEchoTests(EchoTests, TcpTests, unittest.TestCase): - pass - -class TcpPipeliningTests(PipeliningTests, TcpTests, unittest.TestCase): - pass - -class TcpExpectContinueTests(ExpectContinueTests, TcpTests, unittest.TestCase): - pass - -class TcpBadContentLengthTests( - BadContentLengthTests, TcpTests, unittest.TestCase): - pass - -class TcpNoContentLengthTests( - NoContentLengthTests, TcpTests, unittest.TestCase): - pass - -class TcpWriteCallbackTests(WriteCallbackTests, TcpTests, unittest.TestCase): - pass - -class TcpTooLargeTests(TooLargeTests, TcpTests, unittest.TestCase): - pass - -class TcpInternalServerErrorTests( - InternalServerErrorTests, TcpTests, unittest.TestCase): - pass - -class TcpFileWrapperTests(FileWrapperTests, TcpTests, unittest.TestCase): - pass - -if hasattr(socket, 'AF_UNIX'): - - class FixtureUnixWSGIServer(server.UnixWSGIServer): - """A version of UnixWSGIServer that relays back what it's bound to. - """ - - family = socket.AF_UNIX # Testing - - def __init__(self, application, queue, **kw): # pragma: no cover - # Coverage doesn't see this as it's ran in a separate process. - # To permit parallel testing, use a PID-dependent socket. - kw['unix_socket'] = '/tmp/waitress.test-%d.sock' % os.getpid() - super(FixtureUnixWSGIServer, self).__init__(application, **kw) - queue.put(self.socket.getsockname()) - - class UnixTests(SubprocessTests): - - server = FixtureUnixWSGIServer - - def make_http_connection(self): - return UnixHTTPConnection(self.bound_to) - - def stop_subprocess(self): - super(UnixTests, self).stop_subprocess() - cleanup_unix_socket(self.bound_to) - - def send_check_error(self, to_send): - # Unlike inet domain sockets, Unix domain sockets can trigger a - # 'Broken pipe' error when the socket it closed. - try: - self.sock.send(to_send) - except socket.error as exc: - self.assertEqual(get_errno(exc), errno.EPIPE) - - class UnixEchoTests(EchoTests, UnixTests, unittest.TestCase): - pass - - class UnixPipeliningTests(PipeliningTests, UnixTests, unittest.TestCase): - pass - - class UnixExpectContinueTests( - ExpectContinueTests, UnixTests, unittest.TestCase): - pass - - class UnixBadContentLengthTests( - BadContentLengthTests, UnixTests, unittest.TestCase): - pass - - class UnixNoContentLengthTests( - NoContentLengthTests, UnixTests, unittest.TestCase): - pass - - class UnixWriteCallbackTests( - WriteCallbackTests, UnixTests, unittest.TestCase): - pass - - class UnixTooLargeTests(TooLargeTests, UnixTests, unittest.TestCase): - pass - - class UnixInternalServerErrorTests( - InternalServerErrorTests, UnixTests, unittest.TestCase): - pass - - class UnixFileWrapperTests(FileWrapperTests, UnixTests, unittest.TestCase): - pass - -def parse_headers(fp): - """Parses only RFC2822 headers from a file pointer. - """ - headers = {} - while True: - line = fp.readline() - if line in (b'\r\n', b'\n', b''): - break - line = line.decode('iso-8859-1') - name, value = line.strip().split(':', 1) - headers[name.lower().strip()] = value.lower().strip() - return headers - -class UnixHTTPConnection(httplib.HTTPConnection): - """Patched version of HTTPConnection that uses Unix domain sockets. - """ - - def __init__(self, path): - httplib.HTTPConnection.__init__(self, 'localhost') - self.path = path - - def connect(self): - sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) - sock.connect(self.path) - self.sock = sock - -class ConnectionClosed(Exception): - pass - -# stolen from gevent -def read_http(fp): # pragma: no cover - try: - response_line = fp.readline() - except socket.error as exc: - fp.close() - # errno 104 is ENOTRECOVERABLE, In WinSock 10054 is ECONNRESET - if get_errno(exc) in (errno.ECONNABORTED, errno.ECONNRESET, 104, 10054): - raise ConnectionClosed - raise - if not response_line: - raise ConnectionClosed - - header_lines = [] - while True: - line = fp.readline() - if line in (b'\r\n', b'\n', b''): - break - else: - header_lines.append(line) - headers = dict() - for x in header_lines: - x = x.strip() - if not x: - continue - key, value = x.split(b': ', 1) - key = key.decode('iso-8859-1').lower() - value = value.decode('iso-8859-1') - assert key not in headers, "%s header duplicated" % key - headers[key] = value - - if 'content-length' in headers: - num = int(headers['content-length']) - body = b'' - left = num - while left > 0: - data = fp.read(left) - if not data: - break - body += data - left -= len(data) - else: - # read until EOF - body = fp.read() - - return response_line, headers, body - -# stolen from gevent -def get_errno(exc): # pragma: no cover - """ Get the error code out of socket.error objects. - socket.error in <2.5 does not have errno attribute - socket.error in 3.x does not allow indexing access - e.args[0] works for all. - There are cases when args[0] is not errno. - i.e. http://bugs.python.org/issue6471 - Maybe there are cases when errno is set, but it is not the first argument? - """ - try: - if exc.errno is not None: - return exc.errno - except AttributeError: - pass - try: - return exc.args[0] - except IndexError: - return None - -def chunks(l, n): - """ Yield successive n-sized chunks from l. - """ - for i in range(0, len(l), n): - yield l[i:i + n] diff --git a/libs/waitress/tests/test_init.py b/libs/waitress/tests/test_init.py deleted file mode 100644 index 66c34ce8e..000000000 --- a/libs/waitress/tests/test_init.py +++ /dev/null @@ -1,47 +0,0 @@ -import unittest - -class Test_serve(unittest.TestCase): - - def _callFUT(self, app, **kw): - from waitress import serve - return serve(app, **kw) - - def test_it(self): - server = DummyServerFactory() - app = object() - result = self._callFUT(app, _server=server, _quiet=True) - self.assertEqual(server.app, app) - self.assertEqual(result, None) - self.assertEqual(server.ran, True) - -class Test_serve_paste(unittest.TestCase): - - def _callFUT(self, app, **kw): - from waitress import serve_paste - return serve_paste(app, None, **kw) - - def test_it(self): - server = DummyServerFactory() - app = object() - result = self._callFUT(app, _server=server, _quiet=True) - self.assertEqual(server.app, app) - self.assertEqual(result, 0) - self.assertEqual(server.ran, True) - -class DummyServerFactory(object): - ran = False - - def __call__(self, app, **kw): - self.adj = DummyAdj(kw) - self.app = app - self.kw = kw - return self - - def run(self): - self.ran = True - -class DummyAdj(object): - verbose = False - - def __init__(self, kw): - self.__dict__.update(kw) diff --git a/libs/waitress/tests/test_parser.py b/libs/waitress/tests/test_parser.py deleted file mode 100644 index ecb66060a..000000000 --- a/libs/waitress/tests/test_parser.py +++ /dev/null @@ -1,452 +0,0 @@ -############################################################################## -# -# Copyright (c) 2002 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -"""HTTP Request Parser tests -""" -import unittest - -from waitress.compat import ( - text_, - tobytes, -) - -class TestHTTPRequestParser(unittest.TestCase): - - def setUp(self): - from waitress.parser import HTTPRequestParser - from waitress.adjustments import Adjustments - my_adj = Adjustments() - self.parser = HTTPRequestParser(my_adj) - - def test_get_body_stream_None(self): - self.parser.body_recv = None - result = self.parser.get_body_stream() - self.assertEqual(result.getvalue(), b'') - - def test_get_body_stream_nonNone(self): - body_rcv = DummyBodyStream() - self.parser.body_rcv = body_rcv - result = self.parser.get_body_stream() - self.assertEqual(result, body_rcv) - - def test_received_nonsense_with_double_cr(self): - data = b"""\ -HTTP/1.0 GET /foobar - - -""" - result = self.parser.received(data) - self.assertEqual(result, 22) - self.assertTrue(self.parser.completed) - self.assertEqual(self.parser.headers, {}) - - def test_received_bad_host_header(self): - from waitress.utilities import BadRequest - data = b"""\ -HTTP/1.0 GET /foobar - Host: foo - - -""" - result = self.parser.received(data) - self.assertEqual(result, 33) - self.assertTrue(self.parser.completed) - self.assertEqual(self.parser.error.__class__, BadRequest) - - def test_received_nonsense_nothing(self): - data = b"""\ - - -""" - result = self.parser.received(data) - self.assertEqual(result, 2) - self.assertTrue(self.parser.completed) - self.assertEqual(self.parser.headers, {}) - - def test_received_no_doublecr(self): - data = b"""\ -GET /foobar HTTP/8.4 -""" - result = self.parser.received(data) - self.assertEqual(result, 21) - self.assertFalse(self.parser.completed) - self.assertEqual(self.parser.headers, {}) - - def test_received_already_completed(self): - self.parser.completed = True - result = self.parser.received(b'a') - self.assertEqual(result, 0) - - def test_received_cl_too_large(self): - from waitress.utilities import RequestEntityTooLarge - self.parser.adj.max_request_body_size = 2 - data = b"""\ -GET /foobar HTTP/8.4 -Content-Length: 10 - -""" - result = self.parser.received(data) - self.assertEqual(result, 41) - self.assertTrue(self.parser.completed) - self.assertTrue(isinstance(self.parser.error, RequestEntityTooLarge)) - - def test_received_headers_too_large(self): - from waitress.utilities import RequestHeaderFieldsTooLarge - self.parser.adj.max_request_header_size = 2 - data = b"""\ -GET /foobar HTTP/8.4 -X-Foo: 1 -""" - result = self.parser.received(data) - self.assertEqual(result, 30) - self.assertTrue(self.parser.completed) - self.assertTrue(isinstance(self.parser.error, - RequestHeaderFieldsTooLarge)) - - def test_received_body_too_large(self): - from waitress.utilities import RequestEntityTooLarge - self.parser.adj.max_request_body_size = 2 - data = b"""\ -GET /foobar HTTP/1.1 -Transfer-Encoding: chunked -X-Foo: 1 - -20;\r\n -This string has 32 characters\r\n -0\r\n\r\n""" - result = self.parser.received(data) - self.assertEqual(result, 58) - self.parser.received(data[result:]) - self.assertTrue(self.parser.completed) - self.assertTrue(isinstance(self.parser.error, - RequestEntityTooLarge)) - - def test_received_error_from_parser(self): - from waitress.utilities import BadRequest - data = b"""\ -GET /foobar HTTP/1.1 -Transfer-Encoding: chunked -X-Foo: 1 - -garbage -""" - # header - result = self.parser.received(data) - # body - result = self.parser.received(data[result:]) - self.assertEqual(result, 8) - self.assertTrue(self.parser.completed) - self.assertTrue(isinstance(self.parser.error, - BadRequest)) - - def test_received_chunked_completed_sets_content_length(self): - data = b"""\ -GET /foobar HTTP/1.1 -Transfer-Encoding: chunked -X-Foo: 1 - -20;\r\n -This string has 32 characters\r\n -0\r\n\r\n""" - result = self.parser.received(data) - self.assertEqual(result, 58) - data = data[result:] - result = self.parser.received(data) - self.assertTrue(self.parser.completed) - self.assertTrue(self.parser.error is None) - self.assertEqual(self.parser.headers['CONTENT_LENGTH'], '32') - - def test_parse_header_gardenpath(self): - data = b"""\ -GET /foobar HTTP/8.4 -foo: bar""" - self.parser.parse_header(data) - self.assertEqual(self.parser.first_line, b'GET /foobar HTTP/8.4') - self.assertEqual(self.parser.headers['FOO'], 'bar') - - def test_parse_header_no_cr_in_headerplus(self): - data = b"GET /foobar HTTP/8.4" - self.parser.parse_header(data) - self.assertEqual(self.parser.first_line, data) - - def test_parse_header_bad_content_length(self): - data = b"GET /foobar HTTP/8.4\ncontent-length: abc" - self.parser.parse_header(data) - self.assertEqual(self.parser.body_rcv, None) - - def test_parse_header_11_te_chunked(self): - # NB: test that capitalization of header value is unimportant - data = b"GET /foobar HTTP/1.1\ntransfer-encoding: ChUnKed" - self.parser.parse_header(data) - self.assertEqual(self.parser.body_rcv.__class__.__name__, - 'ChunkedReceiver') - - def test_parse_header_11_expect_continue(self): - data = b"GET /foobar HTTP/1.1\nexpect: 100-continue" - self.parser.parse_header(data) - self.assertEqual(self.parser.expect_continue, True) - - def test_parse_header_connection_close(self): - data = b"GET /foobar HTTP/1.1\nConnection: close\n\n" - self.parser.parse_header(data) - self.assertEqual(self.parser.connection_close, True) - - def test_close_with_body_rcv(self): - body_rcv = DummyBodyStream() - self.parser.body_rcv = body_rcv - self.parser.close() - self.assertTrue(body_rcv.closed) - - def test_close_with_no_body_rcv(self): - self.parser.body_rcv = None - self.parser.close() # doesn't raise - -class Test_split_uri(unittest.TestCase): - - def _callFUT(self, uri): - from waitress.parser import split_uri - (self.proxy_scheme, - self.proxy_netloc, - self.path, - self.query, self.fragment) = split_uri(uri) - - def test_split_uri_unquoting_unneeded(self): - self._callFUT(b'http://localhost:8080/abc def') - self.assertEqual(self.path, '/abc def') - - def test_split_uri_unquoting_needed(self): - self._callFUT(b'http://localhost:8080/abc%20def') - self.assertEqual(self.path, '/abc def') - - def test_split_url_with_query(self): - self._callFUT(b'http://localhost:8080/abc?a=1&b=2') - self.assertEqual(self.path, '/abc') - self.assertEqual(self.query, 'a=1&b=2') - - def test_split_url_with_query_empty(self): - self._callFUT(b'http://localhost:8080/abc?') - self.assertEqual(self.path, '/abc') - self.assertEqual(self.query, '') - - def test_split_url_with_fragment(self): - self._callFUT(b'http://localhost:8080/#foo') - self.assertEqual(self.path, '/') - self.assertEqual(self.fragment, 'foo') - - def test_split_url_https(self): - self._callFUT(b'https://localhost:8080/') - self.assertEqual(self.path, '/') - self.assertEqual(self.proxy_scheme, 'https') - self.assertEqual(self.proxy_netloc, 'localhost:8080') - - def test_split_uri_unicode_error_raises_parsing_error(self): - # See https://github.com/Pylons/waitress/issues/64 - from waitress.parser import ParsingError - # Either pass or throw a ParsingError, just don't throw another type of - # exception as that will cause the connection to close badly: - try: - self._callFUT(b'/\xd0') - except ParsingError: - pass - -class Test_get_header_lines(unittest.TestCase): - - def _callFUT(self, data): - from waitress.parser import get_header_lines - return get_header_lines(data) - - def test_get_header_lines(self): - result = self._callFUT(b'slam\nslim') - self.assertEqual(result, [b'slam', b'slim']) - - def test_get_header_lines_folded(self): - # From RFC2616: - # HTTP/1.1 header field values can be folded onto multiple lines if the - # continuation line begins with a space or horizontal tab. All linear - # white space, including folding, has the same semantics as SP. A - # recipient MAY replace any linear white space with a single SP before - # interpreting the field value or forwarding the message downstream. - - # We are just preserving the whitespace that indicates folding. - result = self._callFUT(b'slim\n slam') - self.assertEqual(result, [b'slim slam']) - - def test_get_header_lines_tabbed(self): - result = self._callFUT(b'slam\n\tslim') - self.assertEqual(result, [b'slam\tslim']) - - def test_get_header_lines_malformed(self): - # http://corte.si/posts/code/pathod/pythonservers/index.html - from waitress.parser import ParsingError - self.assertRaises(ParsingError, - self._callFUT, b' Host: localhost\r\n\r\n') - -class Test_crack_first_line(unittest.TestCase): - - def _callFUT(self, line): - from waitress.parser import crack_first_line - return crack_first_line(line) - - def test_crack_first_line_matchok(self): - result = self._callFUT(b'GET / HTTP/1.0') - self.assertEqual(result, (b'GET', b'/', b'1.0')) - - def test_crack_first_line_lowercase_method(self): - from waitress.parser import ParsingError - self.assertRaises(ParsingError, self._callFUT, b'get / HTTP/1.0') - - def test_crack_first_line_nomatch(self): - result = self._callFUT(b'GET / bleh') - self.assertEqual(result, (b'', b'', b'')) - - def test_crack_first_line_missing_version(self): - result = self._callFUT(b'GET /') - self.assertEqual(result, (b'GET', b'/', None)) - -class TestHTTPRequestParserIntegration(unittest.TestCase): - - def setUp(self): - from waitress.parser import HTTPRequestParser - from waitress.adjustments import Adjustments - my_adj = Adjustments() - self.parser = HTTPRequestParser(my_adj) - - def feed(self, data): - parser = self.parser - for n in range(100): # make sure we never loop forever - consumed = parser.received(data) - data = data[consumed:] - if parser.completed: - return - raise ValueError('Looping') # pragma: no cover - - def testSimpleGET(self): - data = b"""\ -GET /foobar HTTP/8.4 -FirstName: mickey -lastname: Mouse -content-length: 7 - -Hello. -""" - parser = self.parser - self.feed(data) - self.assertTrue(parser.completed) - self.assertEqual(parser.version, '8.4') - self.assertFalse(parser.empty) - self.assertEqual(parser.headers, - {'FIRSTNAME': 'mickey', - 'LASTNAME': 'Mouse', - 'CONTENT_LENGTH': '7', - }) - self.assertEqual(parser.path, '/foobar') - self.assertEqual(parser.command, 'GET') - self.assertEqual(parser.query, '') - self.assertEqual(parser.proxy_scheme, '') - self.assertEqual(parser.proxy_netloc, '') - self.assertEqual(parser.get_body_stream().getvalue(), b'Hello.\n') - - def testComplexGET(self): - data = b"""\ -GET /foo/a+%2B%2F%C3%A4%3D%26a%3Aint?d=b+%2B%2F%3D%26b%3Aint&c+%2B%2F%3D%26c%3Aint=6 HTTP/8.4 -FirstName: mickey -lastname: Mouse -content-length: 10 - -Hello mickey. -""" - parser = self.parser - self.feed(data) - self.assertEqual(parser.command, 'GET') - self.assertEqual(parser.version, '8.4') - self.assertFalse(parser.empty) - self.assertEqual(parser.headers, - {'FIRSTNAME': 'mickey', - 'LASTNAME': 'Mouse', - 'CONTENT_LENGTH': '10', - }) - # path should be utf-8 encoded - self.assertEqual(tobytes(parser.path).decode('utf-8'), - text_(b'/foo/a++/\xc3\xa4=&a:int', 'utf-8')) - self.assertEqual(parser.query, - 'd=b+%2B%2F%3D%26b%3Aint&c+%2B%2F%3D%26c%3Aint=6') - self.assertEqual(parser.get_body_stream().getvalue(), b'Hello mick') - - def testProxyGET(self): - data = b"""\ -GET https://example.com:8080/foobar HTTP/8.4 -content-length: 7 - -Hello. -""" - parser = self.parser - self.feed(data) - self.assertTrue(parser.completed) - self.assertEqual(parser.version, '8.4') - self.assertFalse(parser.empty) - self.assertEqual(parser.headers, - {'CONTENT_LENGTH': '7', - }) - self.assertEqual(parser.path, '/foobar') - self.assertEqual(parser.command, 'GET') - self.assertEqual(parser.proxy_scheme, 'https') - self.assertEqual(parser.proxy_netloc, 'example.com:8080') - self.assertEqual(parser.command, 'GET') - self.assertEqual(parser.query, '') - self.assertEqual(parser.get_body_stream().getvalue(), b'Hello.\n') - - def testDuplicateHeaders(self): - # Ensure that headers with the same key get concatenated as per - # RFC2616. - data = b"""\ -GET /foobar HTTP/8.4 -x-forwarded-for: 10.11.12.13 -x-forwarded-for: unknown,127.0.0.1 -X-Forwarded_for: 255.255.255.255 -content-length: 7 - -Hello. -""" - self.feed(data) - self.assertTrue(self.parser.completed) - self.assertEqual(self.parser.headers, { - 'CONTENT_LENGTH': '7', - 'X_FORWARDED_FOR': - '10.11.12.13, unknown,127.0.0.1', - }) - - def testSpoofedHeadersDropped(self): - data = b"""\ -GET /foobar HTTP/8.4 -x-auth_user: bob -content-length: 7 - -Hello. -""" - self.feed(data) - self.assertTrue(self.parser.completed) - self.assertEqual(self.parser.headers, { - 'CONTENT_LENGTH': '7', - }) - - -class DummyBodyStream(object): - - def getfile(self): - return self - - def getbuf(self): - return self - - def close(self): - self.closed = True diff --git a/libs/waitress/tests/test_receiver.py b/libs/waitress/tests/test_receiver.py deleted file mode 100644 index 707f3284f..000000000 --- a/libs/waitress/tests/test_receiver.py +++ /dev/null @@ -1,169 +0,0 @@ -import unittest - -class TestFixedStreamReceiver(unittest.TestCase): - - def _makeOne(self, cl, buf): - from waitress.receiver import FixedStreamReceiver - return FixedStreamReceiver(cl, buf) - - def test_received_remain_lt_1(self): - buf = DummyBuffer() - inst = self._makeOne(0, buf) - result = inst.received('a') - self.assertEqual(result, 0) - self.assertEqual(inst.completed, True) - - def test_received_remain_lte_datalen(self): - buf = DummyBuffer() - inst = self._makeOne(1, buf) - result = inst.received('aa') - self.assertEqual(result, 1) - self.assertEqual(inst.completed, True) - self.assertEqual(inst.completed, 1) - self.assertEqual(inst.remain, 0) - self.assertEqual(buf.data, ['a']) - - def test_received_remain_gt_datalen(self): - buf = DummyBuffer() - inst = self._makeOne(10, buf) - result = inst.received('aa') - self.assertEqual(result, 2) - self.assertEqual(inst.completed, False) - self.assertEqual(inst.remain, 8) - self.assertEqual(buf.data, ['aa']) - - def test_getfile(self): - buf = DummyBuffer() - inst = self._makeOne(10, buf) - self.assertEqual(inst.getfile(), buf) - - def test_getbuf(self): - buf = DummyBuffer() - inst = self._makeOne(10, buf) - self.assertEqual(inst.getbuf(), buf) - - def test___len__(self): - buf = DummyBuffer(['1', '2']) - inst = self._makeOne(10, buf) - self.assertEqual(inst.__len__(), 2) - -class TestChunkedReceiver(unittest.TestCase): - - def _makeOne(self, buf): - from waitress.receiver import ChunkedReceiver - return ChunkedReceiver(buf) - - def test_alreadycompleted(self): - buf = DummyBuffer() - inst = self._makeOne(buf) - inst.completed = True - result = inst.received(b'a') - self.assertEqual(result, 0) - self.assertEqual(inst.completed, True) - - def test_received_remain_gt_zero(self): - buf = DummyBuffer() - inst = self._makeOne(buf) - inst.chunk_remainder = 100 - result = inst.received(b'a') - self.assertEqual(inst.chunk_remainder, 99) - self.assertEqual(result, 1) - self.assertEqual(inst.completed, False) - - def test_received_control_line_notfinished(self): - buf = DummyBuffer() - inst = self._makeOne(buf) - result = inst.received(b'a') - self.assertEqual(inst.control_line, b'a') - self.assertEqual(result, 1) - self.assertEqual(inst.completed, False) - - def test_received_control_line_finished_garbage_in_input(self): - buf = DummyBuffer() - inst = self._makeOne(buf) - result = inst.received(b'garbage\n') - self.assertEqual(result, 8) - self.assertTrue(inst.error) - - def test_received_control_line_finished_all_chunks_not_received(self): - buf = DummyBuffer() - inst = self._makeOne(buf) - result = inst.received(b'a;discard\n') - self.assertEqual(inst.control_line, b'') - self.assertEqual(inst.chunk_remainder, 10) - self.assertEqual(inst.all_chunks_received, False) - self.assertEqual(result, 10) - self.assertEqual(inst.completed, False) - - def test_received_control_line_finished_all_chunks_received(self): - buf = DummyBuffer() - inst = self._makeOne(buf) - result = inst.received(b'0;discard\n') - self.assertEqual(inst.control_line, b'') - self.assertEqual(inst.all_chunks_received, True) - self.assertEqual(result, 10) - self.assertEqual(inst.completed, False) - - def test_received_trailer_startswith_crlf(self): - buf = DummyBuffer() - inst = self._makeOne(buf) - inst.all_chunks_received = True - result = inst.received(b'\r\n') - self.assertEqual(result, 2) - self.assertEqual(inst.completed, True) - - def test_received_trailer_startswith_lf(self): - buf = DummyBuffer() - inst = self._makeOne(buf) - inst.all_chunks_received = True - result = inst.received(b'\n') - self.assertEqual(result, 1) - self.assertEqual(inst.completed, True) - - def test_received_trailer_not_finished(self): - buf = DummyBuffer() - inst = self._makeOne(buf) - inst.all_chunks_received = True - result = inst.received(b'a') - self.assertEqual(result, 1) - self.assertEqual(inst.completed, False) - - def test_received_trailer_finished(self): - buf = DummyBuffer() - inst = self._makeOne(buf) - inst.all_chunks_received = True - result = inst.received(b'abc\r\n\r\n') - self.assertEqual(inst.trailer, b'abc\r\n\r\n') - self.assertEqual(result, 7) - self.assertEqual(inst.completed, True) - - def test_getfile(self): - buf = DummyBuffer() - inst = self._makeOne(buf) - self.assertEqual(inst.getfile(), buf) - - def test_getbuf(self): - buf = DummyBuffer() - inst = self._makeOne(buf) - self.assertEqual(inst.getbuf(), buf) - - def test___len__(self): - buf = DummyBuffer(['1', '2']) - inst = self._makeOne(buf) - self.assertEqual(inst.__len__(), 2) - -class DummyBuffer(object): - - def __init__(self, data=None): - if data is None: - data = [] - self.data = data - - def append(self, s): - self.data.append(s) - - def getfile(self): - return self - - def __len__(self): - return len(self.data) diff --git a/libs/waitress/tests/test_regression.py b/libs/waitress/tests/test_regression.py deleted file mode 100644 index f43895e16..000000000 --- a/libs/waitress/tests/test_regression.py +++ /dev/null @@ -1,144 +0,0 @@ -############################################################################## -# -# Copyright (c) 2005 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -"""Tests for waitress.channel maintenance logic -""" -import doctest - -class FakeSocket: # pragma: no cover - data = '' - setblocking = lambda *_: None - close = lambda *_: None - - def __init__(self, no): - self.no = no - - def fileno(self): - return self.no - - def getpeername(self): - return ('localhost', self.no) - - def send(self, data): - self.data += data - return len(data) - - def recv(self, data): - return 'data' - -def zombies_test(): - """Regression test for HTTPChannel.maintenance method - - Bug: This method checks for channels that have been "inactive" for a - configured time. The bug was that last_activity is set at creation time - but never updated during async channel activity (reads and writes), so - any channel older than the configured timeout will be closed when a new - channel is created, regardless of activity. - - >>> import time - >>> import waitress.adjustments - >>> config = waitress.adjustments.Adjustments() - - >>> from waitress.server import HTTPServer - >>> class TestServer(HTTPServer): - ... def bind(self, (ip, port)): - ... print "Listening on %s:%d" % (ip or '*', port) - >>> sb = TestServer('127.0.0.1', 80, start=False, verbose=True) - Listening on 127.0.0.1:80 - - First we confirm the correct behavior, where a channel with no activity - for the timeout duration gets closed. - - >>> from waitress.channel import HTTPChannel - >>> socket = FakeSocket(42) - >>> channel = HTTPChannel(sb, socket, ('localhost', 42)) - - >>> channel.connected - True - - >>> channel.last_activity -= int(config.channel_timeout) + 1 - - >>> channel.next_channel_cleanup[0] = channel.creation_time - int( - ... config.cleanup_interval) - 1 - - >>> socket2 = FakeSocket(7) - >>> channel2 = HTTPChannel(sb, socket2, ('localhost', 7)) - - >>> channel.connected - False - - Write Activity - -------------- - - Now we make sure that if there is activity the channel doesn't get closed - incorrectly. - - >>> channel2.connected - True - - >>> channel2.last_activity -= int(config.channel_timeout) + 1 - - >>> channel2.handle_write() - - >>> channel2.next_channel_cleanup[0] = channel2.creation_time - int( - ... config.cleanup_interval) - 1 - - >>> socket3 = FakeSocket(3) - >>> channel3 = HTTPChannel(sb, socket3, ('localhost', 3)) - - >>> channel2.connected - True - - Read Activity - -------------- - - We should test to see that read activity will update a channel as well. - - >>> channel3.connected - True - - >>> channel3.last_activity -= int(config.channel_timeout) + 1 - - >>> import waitress.parser - >>> channel3.parser_class = ( - ... waitress.parser.HTTPRequestParser) - >>> channel3.handle_read() - - >>> channel3.next_channel_cleanup[0] = channel3.creation_time - int( - ... config.cleanup_interval) - 1 - - >>> socket4 = FakeSocket(4) - >>> channel4 = HTTPChannel(sb, socket4, ('localhost', 4)) - - >>> channel3.connected - True - - Main loop window - ---------------- - - There is also a corner case we'll do a shallow test for where a - channel can be closed waiting for the main loop. - - >>> channel4.last_activity -= 1 - - >>> last_active = channel4.last_activity - - >>> channel4.set_async() - - >>> channel4.last_activity != last_active - True - -""" - -def test_suite(): - return doctest.DocTestSuite() diff --git a/libs/waitress/tests/test_runner.py b/libs/waitress/tests/test_runner.py deleted file mode 100644 index fa927f0a2..000000000 --- a/libs/waitress/tests/test_runner.py +++ /dev/null @@ -1,205 +0,0 @@ -import contextlib -import os -import sys - -if sys.version_info[:2] == (2, 6): # pragma: no cover - import unittest2 as unittest -else: # pragma: no cover - import unittest - -from waitress import runner - -class Test_match(unittest.TestCase): - - def test_empty(self): - self.assertRaisesRegexp( - ValueError, "^Malformed application ''$", - runner.match, '') - - def test_module_only(self): - self.assertRaisesRegexp( - ValueError, r"^Malformed application 'foo\.bar'$", - runner.match, 'foo.bar') - - def test_bad_module(self): - self.assertRaisesRegexp( - ValueError, - r"^Malformed application 'foo#bar:barney'$", - runner.match, 'foo#bar:barney') - - def test_module_obj(self): - self.assertTupleEqual( - runner.match('foo.bar:fred.barney'), - ('foo.bar', 'fred.barney')) - -class Test_resolve(unittest.TestCase): - - def test_bad_module(self): - self.assertRaises( - ImportError, - runner.resolve, 'nonexistent', 'nonexistent_function') - - def test_nonexistent_function(self): - self.assertRaisesRegexp( - AttributeError, - r"has no attribute 'nonexistent_function'", - runner.resolve, 'os.path', 'nonexistent_function') - - def test_simple_happy_path(self): - from os.path import exists - self.assertIs(runner.resolve('os.path', 'exists'), exists) - - def test_complex_happy_path(self): - # Ensure we can recursively resolve object attributes if necessary. - self.assertEquals( - runner.resolve('os.path', 'exists.__name__'), - 'exists') - -class Test_run(unittest.TestCase): - - def match_output(self, argv, code, regex): - argv = ['waitress-serve'] + argv - with capture() as captured: - self.assertEqual(runner.run(argv=argv), code) - self.assertRegexpMatches(captured.getvalue(), regex) - captured.close() - - def test_bad(self): - self.match_output( - ['--bad-opt'], - 1, - '^Error: option --bad-opt not recognized') - - def test_help(self): - self.match_output( - ['--help'], - 0, - "^Usage:\n\n waitress-serve") - - def test_no_app(self): - self.match_output( - [], - 1, - "^Error: Specify one application only") - - def test_multiple_apps_app(self): - self.match_output( - ['a:a', 'b:b'], - 1, - "^Error: Specify one application only") - - def test_bad_apps_app(self): - self.match_output( - ['a'], - 1, - "^Error: Malformed application 'a'") - - def test_bad_app_module(self): - self.match_output( - ['nonexistent:a'], - 1, - "^Error: Bad module 'nonexistent'") - - self.match_output( - ['nonexistent:a'], - 1, - ( - r"There was an exception \((ImportError|ModuleNotFoundError)\) " - "importing your module.\n\nIt had these arguments: \n" - "1. No module named '?nonexistent'?" - ) - ) - - def test_cwd_added_to_path(self): - def null_serve(app, **kw): - pass - sys_path = sys.path - current_dir = os.getcwd() - try: - os.chdir(os.path.dirname(__file__)) - argv = [ - 'waitress-serve', - 'fixtureapps.runner:app', - ] - self.assertEqual(runner.run(argv=argv, _serve=null_serve), 0) - finally: - sys.path = sys_path - os.chdir(current_dir) - - def test_bad_app_object(self): - self.match_output( - ['waitress.tests.fixtureapps.runner:a'], - 1, - "^Error: Bad object name 'a'") - - def test_simple_call(self): - import waitress.tests.fixtureapps.runner as _apps - def check_server(app, **kw): - self.assertIs(app, _apps.app) - self.assertDictEqual(kw, {'port': '80'}) - argv = [ - 'waitress-serve', - '--port=80', - 'waitress.tests.fixtureapps.runner:app', - ] - self.assertEqual(runner.run(argv=argv, _serve=check_server), 0) - - def test_returned_app(self): - import waitress.tests.fixtureapps.runner as _apps - def check_server(app, **kw): - self.assertIs(app, _apps.app) - self.assertDictEqual(kw, {'port': '80'}) - argv = [ - 'waitress-serve', - '--port=80', - '--call', - 'waitress.tests.fixtureapps.runner:returns_app', - ] - self.assertEqual(runner.run(argv=argv, _serve=check_server), 0) - -class Test_helper(unittest.TestCase): - - def test_exception_logging(self): - from waitress.runner import show_exception - - regex = ( - r"There was an exception \(ImportError\) importing your module." - r"\n\nIt had these arguments: \n1. My reason" - ) - - with capture() as captured: - try: - raise ImportError("My reason") - except ImportError: - self.assertEqual(show_exception(sys.stderr), None) - self.assertRegexpMatches( - captured.getvalue(), - regex - ) - captured.close() - - regex = ( - r"There was an exception \(ImportError\) importing your module." - r"\n\nIt had no arguments." - ) - - with capture() as captured: - try: - raise ImportError - except ImportError: - self.assertEqual(show_exception(sys.stderr), None) - self.assertRegexpMatches( - captured.getvalue(), - regex - ) - captured.close() - -@contextlib.contextmanager -def capture(): - from waitress.compat import NativeIO - fd = NativeIO() - sys.stdout = fd - sys.stderr = fd - yield fd - sys.stdout = sys.__stdout__ - sys.stderr = sys.__stderr__ diff --git a/libs/waitress/tests/test_server.py b/libs/waitress/tests/test_server.py deleted file mode 100644 index 39b90b3d6..000000000 --- a/libs/waitress/tests/test_server.py +++ /dev/null @@ -1,367 +0,0 @@ -import errno -import socket -import unittest - -dummy_app = object() - -class TestWSGIServer(unittest.TestCase): - - def _makeOne(self, application=dummy_app, host='127.0.0.1', port=0, - _dispatcher=None, adj=None, map=None, _start=True, - _sock=None, _server=None): - from waitress.server import create_server - return create_server( - application, - host=host, - port=port, - map=map, - _dispatcher=_dispatcher, - _start=_start, - _sock=_sock) - - def _makeOneWithMap(self, adj=None, _start=True, host='127.0.0.1', - port=0, app=dummy_app): - sock = DummySock() - task_dispatcher = DummyTaskDispatcher() - map = {} - return self._makeOne( - app, - host=host, - port=port, - map=map, - _sock=sock, - _dispatcher=task_dispatcher, - _start=_start, - ) - - def _makeOneWithMulti(self, adj=None, _start=True, - app=dummy_app, listen="127.0.0.1:0 127.0.0.1:0"): - sock = DummySock() - task_dispatcher = DummyTaskDispatcher() - map = {} - from waitress.server import create_server - return create_server( - app, - listen=listen, - map=map, - _dispatcher=task_dispatcher, - _start=_start, - _sock=sock) - - def test_ctor_app_is_None(self): - self.assertRaises(ValueError, self._makeOneWithMap, app=None) - - def test_ctor_start_true(self): - inst = self._makeOneWithMap(_start=True) - self.assertEqual(inst.accepting, True) - self.assertEqual(inst.socket.listened, 1024) - - def test_ctor_makes_dispatcher(self): - inst = self._makeOne(_start=False, map={}) - self.assertEqual(inst.task_dispatcher.__class__.__name__, - 'ThreadedTaskDispatcher') - - def test_ctor_start_false(self): - inst = self._makeOneWithMap(_start=False) - self.assertEqual(inst.accepting, False) - - def test_get_server_name_empty(self): - inst = self._makeOneWithMap(_start=False) - result = inst.get_server_name('') - self.assertTrue(result) - - def test_get_server_name_with_ip(self): - inst = self._makeOneWithMap(_start=False) - result = inst.get_server_name('127.0.0.1') - self.assertTrue(result) - - def test_get_server_name_with_hostname(self): - inst = self._makeOneWithMap(_start=False) - result = inst.get_server_name('fred.flintstone.com') - self.assertEqual(result, 'fred.flintstone.com') - - def test_get_server_name_0000(self): - inst = self._makeOneWithMap(_start=False) - result = inst.get_server_name('0.0.0.0') - self.assertEqual(result, 'localhost') - - def test_get_server_multi(self): - inst = self._makeOneWithMulti() - self.assertEqual(inst.__class__.__name__, 'MultiSocketServer') - - def test_run(self): - inst = self._makeOneWithMap(_start=False) - inst.asyncore = DummyAsyncore() - inst.task_dispatcher = DummyTaskDispatcher() - inst.run() - self.assertTrue(inst.task_dispatcher.was_shutdown) - - def test_run_base_server(self): - inst = self._makeOneWithMulti(_start=False) - inst.asyncore = DummyAsyncore() - inst.task_dispatcher = DummyTaskDispatcher() - inst.run() - self.assertTrue(inst.task_dispatcher.was_shutdown) - - def test_pull_trigger(self): - inst = self._makeOneWithMap(_start=False) - inst.trigger = DummyTrigger() - inst.pull_trigger() - self.assertEqual(inst.trigger.pulled, True) - - def test_add_task(self): - task = DummyTask() - inst = self._makeOneWithMap() - inst.add_task(task) - self.assertEqual(inst.task_dispatcher.tasks, [task]) - self.assertFalse(task.serviced) - - def test_readable_not_accepting(self): - inst = self._makeOneWithMap() - inst.accepting = False - self.assertFalse(inst.readable()) - - def test_readable_maplen_gt_connection_limit(self): - inst = self._makeOneWithMap() - inst.accepting = True - inst.adj = DummyAdj - inst._map = {'a': 1, 'b': 2} - self.assertFalse(inst.readable()) - - def test_readable_maplen_lt_connection_limit(self): - inst = self._makeOneWithMap() - inst.accepting = True - inst.adj = DummyAdj - inst._map = {} - self.assertTrue(inst.readable()) - - def test_readable_maintenance_false(self): - import time - inst = self._makeOneWithMap() - then = time.time() + 1000 - inst.next_channel_cleanup = then - L = [] - inst.maintenance = lambda t: L.append(t) - inst.readable() - self.assertEqual(L, []) - self.assertEqual(inst.next_channel_cleanup, then) - - def test_readable_maintenance_true(self): - inst = self._makeOneWithMap() - inst.next_channel_cleanup = 0 - L = [] - inst.maintenance = lambda t: L.append(t) - inst.readable() - self.assertEqual(len(L), 1) - self.assertNotEqual(inst.next_channel_cleanup, 0) - - def test_writable(self): - inst = self._makeOneWithMap() - self.assertFalse(inst.writable()) - - def test_handle_read(self): - inst = self._makeOneWithMap() - self.assertEqual(inst.handle_read(), None) - - def test_handle_connect(self): - inst = self._makeOneWithMap() - self.assertEqual(inst.handle_connect(), None) - - def test_handle_accept_wouldblock_socket_error(self): - inst = self._makeOneWithMap() - ewouldblock = socket.error(errno.EWOULDBLOCK) - inst.socket = DummySock(toraise=ewouldblock) - inst.handle_accept() - self.assertEqual(inst.socket.accepted, False) - - def test_handle_accept_other_socket_error(self): - inst = self._makeOneWithMap() - eaborted = socket.error(errno.ECONNABORTED) - inst.socket = DummySock(toraise=eaborted) - inst.adj = DummyAdj - def foo(): - raise socket.error - inst.accept = foo - inst.logger = DummyLogger() - inst.handle_accept() - self.assertEqual(inst.socket.accepted, False) - self.assertEqual(len(inst.logger.logged), 1) - - def test_handle_accept_noerror(self): - inst = self._makeOneWithMap() - innersock = DummySock() - inst.socket = DummySock(acceptresult=(innersock, None)) - inst.adj = DummyAdj - L = [] - inst.channel_class = lambda *arg, **kw: L.append(arg) - inst.handle_accept() - self.assertEqual(inst.socket.accepted, True) - self.assertEqual(innersock.opts, [('level', 'optname', 'value')]) - self.assertEqual(L, [(inst, innersock, None, inst.adj)]) - - def test_maintenance(self): - inst = self._makeOneWithMap() - - class DummyChannel(object): - requests = [] - zombie = DummyChannel() - zombie.last_activity = 0 - zombie.running_tasks = False - inst.active_channels[100] = zombie - inst.maintenance(10000) - self.assertEqual(zombie.will_close, True) - - def test_backward_compatibility(self): - from waitress.server import WSGIServer, TcpWSGIServer - from waitress.adjustments import Adjustments - self.assertTrue(WSGIServer is TcpWSGIServer) - inst = WSGIServer(None, _start=False, port=1234) - # Ensure the adjustment was actually applied. - self.assertNotEqual(Adjustments.port, 1234) - self.assertEqual(inst.adj.port, 1234) - -if hasattr(socket, 'AF_UNIX'): - - class TestUnixWSGIServer(unittest.TestCase): - unix_socket = '/tmp/waitress.test.sock' - - def _makeOne(self, _start=True, _sock=None): - from waitress.server import create_server - return create_server( - dummy_app, - map={}, - _start=_start, - _sock=_sock, - _dispatcher=DummyTaskDispatcher(), - unix_socket=self.unix_socket, - unix_socket_perms='600' - ) - - def _makeDummy(self, *args, **kwargs): - sock = DummySock(*args, **kwargs) - sock.family = socket.AF_UNIX - return sock - - def test_unix(self): - inst = self._makeOne(_start=False) - self.assertEqual(inst.socket.family, socket.AF_UNIX) - self.assertEqual(inst.socket.getsockname(), self.unix_socket) - - def test_handle_accept(self): - # Working on the assumption that we only have to test the happy path - # for Unix domain sockets as the other paths should've been covered - # by inet sockets. - client = self._makeDummy() - listen = self._makeDummy(acceptresult=(client, None)) - inst = self._makeOne(_sock=listen) - self.assertEqual(inst.accepting, True) - self.assertEqual(inst.socket.listened, 1024) - L = [] - inst.channel_class = lambda *arg, **kw: L.append(arg) - inst.handle_accept() - self.assertEqual(inst.socket.accepted, True) - self.assertEqual(client.opts, []) - self.assertEqual( - L, - [(inst, client, ('localhost', None), inst.adj)] - ) - - def test_creates_new_sockinfo(self): - from waitress.server import UnixWSGIServer - inst = UnixWSGIServer( - dummy_app, - unix_socket=self.unix_socket, - unix_socket_perms='600' - ) - - self.assertEqual(inst.sockinfo[0], socket.AF_UNIX) - -class DummySock(object): - accepted = False - blocking = False - family = socket.AF_INET - - def __init__(self, toraise=None, acceptresult=(None, None)): - self.toraise = toraise - self.acceptresult = acceptresult - self.bound = None - self.opts = [] - - def bind(self, addr): - self.bound = addr - - def accept(self): - if self.toraise: - raise self.toraise - self.accepted = True - return self.acceptresult - - def setblocking(self, x): - self.blocking = True - - def fileno(self): - return 10 - - def getpeername(self): - return '127.0.0.1' - - def setsockopt(self, *arg): - self.opts.append(arg) - - def getsockopt(self, *arg): - return 1 - - def listen(self, num): - self.listened = num - - def getsockname(self): - return self.bound - -class DummyTaskDispatcher(object): - - def __init__(self): - self.tasks = [] - - def add_task(self, task): - self.tasks.append(task) - - def shutdown(self): - self.was_shutdown = True - -class DummyTask(object): - serviced = False - start_response_called = False - wrote_header = False - status = '200 OK' - - def __init__(self): - self.response_headers = {} - self.written = '' - - def service(self): # pragma: no cover - self.serviced = True - -class DummyAdj: - connection_limit = 1 - log_socket_errors = True - socket_options = [('level', 'optname', 'value')] - cleanup_interval = 900 - channel_timeout = 300 - -class DummyAsyncore(object): - - def loop(self, timeout=30.0, use_poll=False, map=None, count=None): - raise SystemExit - -class DummyTrigger(object): - - def pull_trigger(self): - self.pulled = True - -class DummyLogger(object): - - def __init__(self): - self.logged = [] - - def warning(self, msg, **kw): - self.logged.append(msg) diff --git a/libs/waitress/tests/test_task.py b/libs/waitress/tests/test_task.py deleted file mode 100644 index 2a2759a23..000000000 --- a/libs/waitress/tests/test_task.py +++ /dev/null @@ -1,931 +0,0 @@ -import unittest -import io - -class TestThreadedTaskDispatcher(unittest.TestCase): - - def _makeOne(self): - from waitress.task import ThreadedTaskDispatcher - return ThreadedTaskDispatcher() - - def test_handler_thread_task_is_None(self): - inst = self._makeOne() - inst.threads[0] = True - inst.queue.put(None) - inst.handler_thread(0) - self.assertEqual(inst.stop_count, -1) - self.assertEqual(inst.threads, {}) - - def test_handler_thread_task_raises(self): - from waitress.task import JustTesting - inst = self._makeOne() - inst.threads[0] = True - inst.logger = DummyLogger() - task = DummyTask(JustTesting) - inst.logger = DummyLogger() - inst.queue.put(task) - inst.handler_thread(0) - self.assertEqual(inst.stop_count, -1) - self.assertEqual(inst.threads, {}) - self.assertEqual(len(inst.logger.logged), 1) - - def test_set_thread_count_increase(self): - inst = self._makeOne() - L = [] - inst.start_new_thread = lambda *x: L.append(x) - inst.set_thread_count(1) - self.assertEqual(L, [(inst.handler_thread, (0,))]) - - def test_set_thread_count_increase_with_existing(self): - inst = self._makeOne() - L = [] - inst.threads = {0: 1} - inst.start_new_thread = lambda *x: L.append(x) - inst.set_thread_count(2) - self.assertEqual(L, [(inst.handler_thread, (1,))]) - - def test_set_thread_count_decrease(self): - inst = self._makeOne() - inst.threads = {'a': 1, 'b': 2} - inst.set_thread_count(1) - self.assertEqual(inst.queue.qsize(), 1) - self.assertEqual(inst.queue.get(), None) - - def test_set_thread_count_same(self): - inst = self._makeOne() - L = [] - inst.start_new_thread = lambda *x: L.append(x) - inst.threads = {0: 1} - inst.set_thread_count(1) - self.assertEqual(L, []) - - def test_add_task(self): - task = DummyTask() - inst = self._makeOne() - inst.add_task(task) - self.assertEqual(inst.queue.qsize(), 1) - self.assertTrue(task.deferred) - - def test_add_task_defer_raises(self): - task = DummyTask(ValueError) - inst = self._makeOne() - self.assertRaises(ValueError, inst.add_task, task) - self.assertEqual(inst.queue.qsize(), 0) - self.assertTrue(task.deferred) - self.assertTrue(task.cancelled) - - def test_shutdown_one_thread(self): - inst = self._makeOne() - inst.threads[0] = 1 - inst.logger = DummyLogger() - task = DummyTask() - inst.queue.put(task) - self.assertEqual(inst.shutdown(timeout=.01), True) - self.assertEqual(inst.logger.logged, ['1 thread(s) still running']) - self.assertEqual(task.cancelled, True) - - def test_shutdown_no_threads(self): - inst = self._makeOne() - self.assertEqual(inst.shutdown(timeout=.01), True) - - def test_shutdown_no_cancel_pending(self): - inst = self._makeOne() - self.assertEqual(inst.shutdown(cancel_pending=False, timeout=.01), - False) - -class TestTask(unittest.TestCase): - - def _makeOne(self, channel=None, request=None): - if channel is None: - channel = DummyChannel() - if request is None: - request = DummyParser() - from waitress.task import Task - return Task(channel, request) - - def test_ctor_version_not_in_known(self): - request = DummyParser() - request.version = '8.4' - inst = self._makeOne(request=request) - self.assertEqual(inst.version, '1.0') - - def test_cancel(self): - inst = self._makeOne() - inst.cancel() - self.assertTrue(inst.close_on_finish) - - def test_defer(self): - inst = self._makeOne() - self.assertEqual(inst.defer(), None) - - def test_build_response_header_bad_http_version(self): - inst = self._makeOne() - inst.request = DummyParser() - inst.version = '8.4' - self.assertRaises(AssertionError, inst.build_response_header) - - def test_build_response_header_v10_keepalive_no_content_length(self): - inst = self._makeOne() - inst.request = DummyParser() - inst.request.headers['CONNECTION'] = 'keep-alive' - inst.version = '1.0' - result = inst.build_response_header() - lines = filter_lines(result) - self.assertEqual(len(lines), 4) - self.assertEqual(lines[0], b'HTTP/1.0 200 OK') - self.assertEqual(lines[1], b'Connection: close') - self.assertTrue(lines[2].startswith(b'Date:')) - self.assertEqual(lines[3], b'Server: waitress') - self.assertEqual(inst.close_on_finish, True) - self.assertTrue(('Connection', 'close') in inst.response_headers) - - def test_build_response_header_v10_keepalive_with_content_length(self): - inst = self._makeOne() - inst.request = DummyParser() - inst.request.headers['CONNECTION'] = 'keep-alive' - inst.response_headers = [('Content-Length', '10')] - inst.version = '1.0' - inst.content_length = 0 - result = inst.build_response_header() - lines = filter_lines(result) - self.assertEqual(len(lines), 5) - self.assertEqual(lines[0], b'HTTP/1.0 200 OK') - self.assertEqual(lines[1], b'Connection: Keep-Alive') - self.assertEqual(lines[2], b'Content-Length: 10') - self.assertTrue(lines[3].startswith(b'Date:')) - self.assertEqual(lines[4], b'Server: waitress') - self.assertEqual(inst.close_on_finish, False) - - def test_build_response_header_v11_connection_closed_by_client(self): - inst = self._makeOne() - inst.request = DummyParser() - inst.version = '1.1' - inst.request.headers['CONNECTION'] = 'close' - result = inst.build_response_header() - lines = filter_lines(result) - self.assertEqual(len(lines), 5) - self.assertEqual(lines[0], b'HTTP/1.1 200 OK') - self.assertEqual(lines[1], b'Connection: close') - self.assertTrue(lines[2].startswith(b'Date:')) - self.assertEqual(lines[3], b'Server: waitress') - self.assertEqual(lines[4], b'Transfer-Encoding: chunked') - self.assertTrue(('Connection', 'close') in inst.response_headers) - self.assertEqual(inst.close_on_finish, True) - - def test_build_response_header_v11_connection_keepalive_by_client(self): - inst = self._makeOne() - inst.request = DummyParser() - inst.request.headers['CONNECTION'] = 'keep-alive' - inst.version = '1.1' - result = inst.build_response_header() - lines = filter_lines(result) - self.assertEqual(len(lines), 5) - self.assertEqual(lines[0], b'HTTP/1.1 200 OK') - self.assertEqual(lines[1], b'Connection: close') - self.assertTrue(lines[2].startswith(b'Date:')) - self.assertEqual(lines[3], b'Server: waitress') - self.assertEqual(lines[4], b'Transfer-Encoding: chunked') - self.assertTrue(('Connection', 'close') in inst.response_headers) - self.assertEqual(inst.close_on_finish, True) - - def test_build_response_header_v11_200_no_content_length(self): - inst = self._makeOne() - inst.request = DummyParser() - inst.version = '1.1' - result = inst.build_response_header() - lines = filter_lines(result) - self.assertEqual(len(lines), 5) - self.assertEqual(lines[0], b'HTTP/1.1 200 OK') - self.assertEqual(lines[1], b'Connection: close') - self.assertTrue(lines[2].startswith(b'Date:')) - self.assertEqual(lines[3], b'Server: waitress') - self.assertEqual(lines[4], b'Transfer-Encoding: chunked') - self.assertEqual(inst.close_on_finish, True) - self.assertTrue(('Connection', 'close') in inst.response_headers) - - def test_build_response_header_via_added(self): - inst = self._makeOne() - inst.request = DummyParser() - inst.version = '1.0' - inst.response_headers = [('Server', 'abc')] - result = inst.build_response_header() - lines = filter_lines(result) - self.assertEqual(len(lines), 5) - self.assertEqual(lines[0], b'HTTP/1.0 200 OK') - self.assertEqual(lines[1], b'Connection: close') - self.assertTrue(lines[2].startswith(b'Date:')) - self.assertEqual(lines[3], b'Server: abc') - self.assertEqual(lines[4], b'Via: waitress') - - def test_build_response_header_date_exists(self): - inst = self._makeOne() - inst.request = DummyParser() - inst.version = '1.0' - inst.response_headers = [('Date', 'date')] - result = inst.build_response_header() - lines = filter_lines(result) - self.assertEqual(len(lines), 4) - self.assertEqual(lines[0], b'HTTP/1.0 200 OK') - self.assertEqual(lines[1], b'Connection: close') - self.assertTrue(lines[2].startswith(b'Date:')) - self.assertEqual(lines[3], b'Server: waitress') - - def test_build_response_header_preexisting_content_length(self): - inst = self._makeOne() - inst.request = DummyParser() - inst.version = '1.1' - inst.content_length = 100 - result = inst.build_response_header() - lines = filter_lines(result) - self.assertEqual(len(lines), 4) - self.assertEqual(lines[0], b'HTTP/1.1 200 OK') - self.assertEqual(lines[1], b'Content-Length: 100') - self.assertTrue(lines[2].startswith(b'Date:')) - self.assertEqual(lines[3], b'Server: waitress') - - def test_remove_content_length_header(self): - inst = self._makeOne() - inst.response_headers = [('Content-Length', '70')] - inst.remove_content_length_header() - self.assertEqual(inst.response_headers, []) - - def test_start(self): - inst = self._makeOne() - inst.start() - self.assertTrue(inst.start_time) - - def test_finish_didnt_write_header(self): - inst = self._makeOne() - inst.wrote_header = False - inst.complete = True - inst.finish() - self.assertTrue(inst.channel.written) - - def test_finish_wrote_header(self): - inst = self._makeOne() - inst.wrote_header = True - inst.finish() - self.assertFalse(inst.channel.written) - - def test_finish_chunked_response(self): - inst = self._makeOne() - inst.wrote_header = True - inst.chunked_response = True - inst.finish() - self.assertEqual(inst.channel.written, b'0\r\n\r\n') - - def test_write_wrote_header(self): - inst = self._makeOne() - inst.wrote_header = True - inst.complete = True - inst.content_length = 3 - inst.write(b'abc') - self.assertEqual(inst.channel.written, b'abc') - - def test_write_header_not_written(self): - inst = self._makeOne() - inst.wrote_header = False - inst.complete = True - inst.write(b'abc') - self.assertTrue(inst.channel.written) - self.assertEqual(inst.wrote_header, True) - - def test_write_start_response_uncalled(self): - inst = self._makeOne() - self.assertRaises(RuntimeError, inst.write, b'') - - def test_write_chunked_response(self): - inst = self._makeOne() - inst.wrote_header = True - inst.chunked_response = True - inst.complete = True - inst.write(b'abc') - self.assertEqual(inst.channel.written, b'3\r\nabc\r\n') - - def test_write_preexisting_content_length(self): - inst = self._makeOne() - inst.wrote_header = True - inst.complete = True - inst.content_length = 1 - inst.logger = DummyLogger() - inst.write(b'abc') - self.assertTrue(inst.channel.written) - self.assertEqual(inst.logged_write_excess, True) - self.assertEqual(len(inst.logger.logged), 1) - -class TestWSGITask(unittest.TestCase): - - def _makeOne(self, channel=None, request=None): - if channel is None: - channel = DummyChannel() - if request is None: - request = DummyParser() - from waitress.task import WSGITask - return WSGITask(channel, request) - - def test_service(self): - inst = self._makeOne() - def execute(): - inst.executed = True - inst.execute = execute - inst.complete = True - inst.service() - self.assertTrue(inst.start_time) - self.assertTrue(inst.close_on_finish) - self.assertTrue(inst.channel.written) - self.assertEqual(inst.executed, True) - - def test_service_server_raises_socket_error(self): - import socket - inst = self._makeOne() - def execute(): - raise socket.error - inst.execute = execute - self.assertRaises(socket.error, inst.service) - self.assertTrue(inst.start_time) - self.assertTrue(inst.close_on_finish) - self.assertFalse(inst.channel.written) - - def test_execute_app_calls_start_response_twice_wo_exc_info(self): - def app(environ, start_response): - start_response('200 OK', []) - start_response('200 OK', []) - inst = self._makeOne() - inst.channel.server.application = app - self.assertRaises(AssertionError, inst.execute) - - def test_execute_app_calls_start_response_w_exc_info_complete(self): - def app(environ, start_response): - start_response('200 OK', [], [ValueError, ValueError(), None]) - return [b'a'] - inst = self._makeOne() - inst.complete = True - inst.channel.server.application = app - inst.execute() - self.assertTrue(inst.complete) - self.assertEqual(inst.status, '200 OK') - self.assertTrue(inst.channel.written) - - def test_execute_app_calls_start_response_w_excinf_headers_unwritten(self): - def app(environ, start_response): - start_response('200 OK', [], [ValueError, None, None]) - return [b'a'] - inst = self._makeOne() - inst.wrote_header = False - inst.channel.server.application = app - inst.response_headers = [('a', 'b')] - inst.execute() - self.assertTrue(inst.complete) - self.assertEqual(inst.status, '200 OK') - self.assertTrue(inst.channel.written) - self.assertFalse(('a','b') in inst.response_headers) - - def test_execute_app_calls_start_response_w_excinf_headers_written(self): - def app(environ, start_response): - start_response('200 OK', [], [ValueError, ValueError(), None]) - inst = self._makeOne() - inst.complete = True - inst.wrote_header = True - inst.channel.server.application = app - self.assertRaises(ValueError, inst.execute) - - def test_execute_bad_header_key(self): - def app(environ, start_response): - start_response('200 OK', [(None, 'a')]) - inst = self._makeOne() - inst.channel.server.application = app - self.assertRaises(AssertionError, inst.execute) - - def test_execute_bad_header_value(self): - def app(environ, start_response): - start_response('200 OK', [('a', None)]) - inst = self._makeOne() - inst.channel.server.application = app - self.assertRaises(AssertionError, inst.execute) - - def test_execute_hopbyhop_header(self): - def app(environ, start_response): - start_response('200 OK', [('Connection', 'close')]) - inst = self._makeOne() - inst.channel.server.application = app - self.assertRaises(AssertionError, inst.execute) - - def test_execute_bad_header_value_control_characters(self): - def app(environ, start_response): - start_response('200 OK', [('a', '\n')]) - inst = self._makeOne() - inst.channel.server.application = app - self.assertRaises(ValueError, inst.execute) - - def test_execute_bad_header_name_control_characters(self): - def app(environ, start_response): - start_response('200 OK', [('a\r', 'value')]) - inst = self._makeOne() - inst.channel.server.application = app - self.assertRaises(ValueError, inst.execute) - - def test_execute_bad_status_control_characters(self): - def app(environ, start_response): - start_response('200 OK\r', []) - inst = self._makeOne() - inst.channel.server.application = app - self.assertRaises(ValueError, inst.execute) - - def test_preserve_header_value_order(self): - def app(environ, start_response): - write = start_response('200 OK', [('C', 'b'), ('A', 'b'), ('A', 'a')]) - write(b'abc') - return [] - inst = self._makeOne() - inst.channel.server.application = app - inst.execute() - self.assertTrue(b'A: b\r\nA: a\r\nC: b\r\n' in inst.channel.written) - - def test_execute_bad_status_value(self): - def app(environ, start_response): - start_response(None, []) - inst = self._makeOne() - inst.channel.server.application = app - self.assertRaises(AssertionError, inst.execute) - - def test_execute_with_content_length_header(self): - def app(environ, start_response): - start_response('200 OK', [('Content-Length', '1')]) - return [b'a'] - inst = self._makeOne() - inst.channel.server.application = app - inst.execute() - self.assertEqual(inst.content_length, 1) - - def test_execute_app_calls_write(self): - def app(environ, start_response): - write = start_response('200 OK', [('Content-Length', '3')]) - write(b'abc') - return [] - inst = self._makeOne() - inst.channel.server.application = app - inst.execute() - self.assertEqual(inst.channel.written[-3:], b'abc') - - def test_execute_app_returns_len1_chunk_without_cl(self): - def app(environ, start_response): - start_response('200 OK', []) - return [b'abc'] - inst = self._makeOne() - inst.channel.server.application = app - inst.execute() - self.assertEqual(inst.content_length, 3) - - def test_execute_app_returns_empty_chunk_as_first(self): - def app(environ, start_response): - start_response('200 OK', []) - return ['', b'abc'] - inst = self._makeOne() - inst.channel.server.application = app - inst.execute() - self.assertEqual(inst.content_length, None) - - def test_execute_app_returns_too_many_bytes(self): - def app(environ, start_response): - start_response('200 OK', [('Content-Length', '1')]) - return [b'abc'] - inst = self._makeOne() - inst.channel.server.application = app - inst.logger = DummyLogger() - inst.execute() - self.assertEqual(inst.close_on_finish, True) - self.assertEqual(len(inst.logger.logged), 1) - - def test_execute_app_returns_too_few_bytes(self): - def app(environ, start_response): - start_response('200 OK', [('Content-Length', '3')]) - return [b'a'] - inst = self._makeOne() - inst.channel.server.application = app - inst.logger = DummyLogger() - inst.execute() - self.assertEqual(inst.close_on_finish, True) - self.assertEqual(len(inst.logger.logged), 1) - - def test_execute_app_do_not_warn_on_head(self): - def app(environ, start_response): - start_response('200 OK', [('Content-Length', '3')]) - return [b''] - inst = self._makeOne() - inst.request.command = 'HEAD' - inst.channel.server.application = app - inst.logger = DummyLogger() - inst.execute() - self.assertEqual(inst.close_on_finish, True) - self.assertEqual(len(inst.logger.logged), 0) - - def test_execute_app_returns_closeable(self): - class closeable(list): - def close(self): - self.closed = True - foo = closeable([b'abc']) - def app(environ, start_response): - start_response('200 OK', [('Content-Length', '3')]) - return foo - inst = self._makeOne() - inst.channel.server.application = app - inst.execute() - self.assertEqual(foo.closed, True) - - def test_execute_app_returns_filewrapper_prepare_returns_True(self): - from waitress.buffers import ReadOnlyFileBasedBuffer - f = io.BytesIO(b'abc') - app_iter = ReadOnlyFileBasedBuffer(f, 8192) - def app(environ, start_response): - start_response('200 OK', [('Content-Length', '3')]) - return app_iter - inst = self._makeOne() - inst.channel.server.application = app - inst.execute() - self.assertTrue(inst.channel.written) # header - self.assertEqual(inst.channel.otherdata, [app_iter]) - - def test_execute_app_returns_filewrapper_prepare_returns_True_nocl(self): - from waitress.buffers import ReadOnlyFileBasedBuffer - f = io.BytesIO(b'abc') - app_iter = ReadOnlyFileBasedBuffer(f, 8192) - def app(environ, start_response): - start_response('200 OK', []) - return app_iter - inst = self._makeOne() - inst.channel.server.application = app - inst.execute() - self.assertTrue(inst.channel.written) # header - self.assertEqual(inst.channel.otherdata, [app_iter]) - self.assertEqual(inst.content_length, 3) - - def test_execute_app_returns_filewrapper_prepare_returns_True_badcl(self): - from waitress.buffers import ReadOnlyFileBasedBuffer - f = io.BytesIO(b'abc') - app_iter = ReadOnlyFileBasedBuffer(f, 8192) - def app(environ, start_response): - start_response('200 OK', []) - return app_iter - inst = self._makeOne() - inst.channel.server.application = app - inst.content_length = 10 - inst.response_headers = [('Content-Length', '10')] - inst.execute() - self.assertTrue(inst.channel.written) # header - self.assertEqual(inst.channel.otherdata, [app_iter]) - self.assertEqual(inst.content_length, 3) - self.assertEqual(dict(inst.response_headers)['Content-Length'], '3') - - def test_get_environment_already_cached(self): - inst = self._makeOne() - inst.environ = object() - self.assertEqual(inst.get_environment(), inst.environ) - - def test_get_environment_path_startswith_more_than_one_slash(self): - inst = self._makeOne() - request = DummyParser() - request.path = '///abc' - inst.request = request - environ = inst.get_environment() - self.assertEqual(environ['PATH_INFO'], '/abc') - - def test_get_environment_path_empty(self): - inst = self._makeOne() - request = DummyParser() - request.path = '' - inst.request = request - environ = inst.get_environment() - self.assertEqual(environ['PATH_INFO'], '') - - def test_get_environment_no_query(self): - inst = self._makeOne() - request = DummyParser() - inst.request = request - environ = inst.get_environment() - self.assertEqual(environ['QUERY_STRING'], '') - - def test_get_environment_with_query(self): - inst = self._makeOne() - request = DummyParser() - request.query = 'abc' - inst.request = request - environ = inst.get_environment() - self.assertEqual(environ['QUERY_STRING'], 'abc') - - def test_get_environ_with_url_prefix_miss(self): - inst = self._makeOne() - inst.channel.server.adj.url_prefix = '/foo' - request = DummyParser() - request.path = '/bar' - inst.request = request - environ = inst.get_environment() - self.assertEqual(environ['PATH_INFO'], '/bar') - self.assertEqual(environ['SCRIPT_NAME'], '/foo') - - def test_get_environ_with_url_prefix_hit(self): - inst = self._makeOne() - inst.channel.server.adj.url_prefix = '/foo' - request = DummyParser() - request.path = '/foo/fuz' - inst.request = request - environ = inst.get_environment() - self.assertEqual(environ['PATH_INFO'], '/fuz') - self.assertEqual(environ['SCRIPT_NAME'], '/foo') - - def test_get_environ_with_url_prefix_empty_path(self): - inst = self._makeOne() - inst.channel.server.adj.url_prefix = '/foo' - request = DummyParser() - request.path = '/foo' - inst.request = request - environ = inst.get_environment() - self.assertEqual(environ['PATH_INFO'], '') - self.assertEqual(environ['SCRIPT_NAME'], '/foo') - - def test_get_environment_values(self): - import sys - inst = self._makeOne() - request = DummyParser() - request.headers = { - 'CONTENT_TYPE': 'abc', - 'CONTENT_LENGTH': '10', - 'X_FOO': 'BAR', - 'CONNECTION': 'close', - } - request.query = 'abc' - inst.request = request - environ = inst.get_environment() - - # nail the keys of environ - self.assertEqual(sorted(environ.keys()), [ - 'CONTENT_LENGTH', 'CONTENT_TYPE', 'HTTP_CONNECTION', 'HTTP_X_FOO', - 'PATH_INFO', 'QUERY_STRING', 'REMOTE_ADDR', 'REQUEST_METHOD', - 'SCRIPT_NAME', 'SERVER_NAME', 'SERVER_PORT', 'SERVER_PROTOCOL', - 'SERVER_SOFTWARE', 'wsgi.errors', 'wsgi.file_wrapper', 'wsgi.input', - 'wsgi.multiprocess', 'wsgi.multithread', 'wsgi.run_once', - 'wsgi.url_scheme', 'wsgi.version']) - - self.assertEqual(environ['REQUEST_METHOD'], 'GET') - self.assertEqual(environ['SERVER_PORT'], '80') - self.assertEqual(environ['SERVER_NAME'], 'localhost') - self.assertEqual(environ['SERVER_SOFTWARE'], 'waitress') - self.assertEqual(environ['SERVER_PROTOCOL'], 'HTTP/1.0') - self.assertEqual(environ['SCRIPT_NAME'], '') - self.assertEqual(environ['HTTP_CONNECTION'], 'close') - self.assertEqual(environ['PATH_INFO'], '/') - self.assertEqual(environ['QUERY_STRING'], 'abc') - self.assertEqual(environ['REMOTE_ADDR'], '127.0.0.1') - self.assertEqual(environ['CONTENT_TYPE'], 'abc') - self.assertEqual(environ['CONTENT_LENGTH'], '10') - self.assertEqual(environ['HTTP_X_FOO'], 'BAR') - self.assertEqual(environ['wsgi.version'], (1, 0)) - self.assertEqual(environ['wsgi.url_scheme'], 'http') - self.assertEqual(environ['wsgi.errors'], sys.stderr) - self.assertEqual(environ['wsgi.multithread'], True) - self.assertEqual(environ['wsgi.multiprocess'], False) - self.assertEqual(environ['wsgi.run_once'], False) - self.assertEqual(environ['wsgi.input'], 'stream') - self.assertEqual(inst.environ, environ) - - def test_get_environment_values_w_scheme_override_untrusted(self): - inst = self._makeOne() - request = DummyParser() - request.headers = { - 'CONTENT_TYPE': 'abc', - 'CONTENT_LENGTH': '10', - 'X_FOO': 'BAR', - 'X_FORWARDED_PROTO': 'https', - 'CONNECTION': 'close', - } - request.query = 'abc' - inst.request = request - environ = inst.get_environment() - self.assertEqual(environ['wsgi.url_scheme'], 'http') - - def test_get_environment_values_w_scheme_override_trusted(self): - import sys - inst = self._makeOne() - inst.channel.addr = ['192.168.1.1'] - inst.channel.server.adj.trusted_proxy = '192.168.1.1' - request = DummyParser() - request.headers = { - 'CONTENT_TYPE': 'abc', - 'CONTENT_LENGTH': '10', - 'X_FOO': 'BAR', - 'X_FORWARDED_PROTO': 'https', - 'CONNECTION': 'close', - } - request.query = 'abc' - inst.request = request - environ = inst.get_environment() - - # nail the keys of environ - self.assertEqual(sorted(environ.keys()), [ - 'CONTENT_LENGTH', 'CONTENT_TYPE', 'HTTP_CONNECTION', 'HTTP_X_FOO', - 'PATH_INFO', 'QUERY_STRING', 'REMOTE_ADDR', 'REQUEST_METHOD', - 'SCRIPT_NAME', 'SERVER_NAME', 'SERVER_PORT', 'SERVER_PROTOCOL', - 'SERVER_SOFTWARE', 'wsgi.errors', 'wsgi.file_wrapper', 'wsgi.input', - 'wsgi.multiprocess', 'wsgi.multithread', 'wsgi.run_once', - 'wsgi.url_scheme', 'wsgi.version']) - - self.assertEqual(environ['REQUEST_METHOD'], 'GET') - self.assertEqual(environ['SERVER_PORT'], '80') - self.assertEqual(environ['SERVER_NAME'], 'localhost') - self.assertEqual(environ['SERVER_SOFTWARE'], 'waitress') - self.assertEqual(environ['SERVER_PROTOCOL'], 'HTTP/1.0') - self.assertEqual(environ['SCRIPT_NAME'], '') - self.assertEqual(environ['HTTP_CONNECTION'], 'close') - self.assertEqual(environ['PATH_INFO'], '/') - self.assertEqual(environ['QUERY_STRING'], 'abc') - self.assertEqual(environ['REMOTE_ADDR'], '192.168.1.1') - self.assertEqual(environ['CONTENT_TYPE'], 'abc') - self.assertEqual(environ['CONTENT_LENGTH'], '10') - self.assertEqual(environ['HTTP_X_FOO'], 'BAR') - self.assertEqual(environ['wsgi.version'], (1, 0)) - self.assertEqual(environ['wsgi.url_scheme'], 'https') - self.assertEqual(environ['wsgi.errors'], sys.stderr) - self.assertEqual(environ['wsgi.multithread'], True) - self.assertEqual(environ['wsgi.multiprocess'], False) - self.assertEqual(environ['wsgi.run_once'], False) - self.assertEqual(environ['wsgi.input'], 'stream') - self.assertEqual(inst.environ, environ) - - def test_get_environment_values_w_bogus_scheme_override(self): - inst = self._makeOne() - inst.channel.addr = ['192.168.1.1'] - inst.channel.server.adj.trusted_proxy = '192.168.1.1' - request = DummyParser() - request.headers = { - 'CONTENT_TYPE': 'abc', - 'CONTENT_LENGTH': '10', - 'X_FOO': 'BAR', - 'X_FORWARDED_PROTO': 'http://p02n3e.com?url=http', - 'CONNECTION': 'close', - } - request.query = 'abc' - inst.request = request - self.assertRaises(ValueError, inst.get_environment) - -class TestErrorTask(unittest.TestCase): - - def _makeOne(self, channel=None, request=None): - if channel is None: - channel = DummyChannel() - if request is None: - request = DummyParser() - request.error = DummyError() - from waitress.task import ErrorTask - return ErrorTask(channel, request) - - def test_execute_http_10(self): - inst = self._makeOne() - inst.execute() - lines = filter_lines(inst.channel.written) - self.assertEqual(len(lines), 9) - self.assertEqual(lines[0], b'HTTP/1.0 432 Too Ugly') - self.assertEqual(lines[1], b'Connection: close') - self.assertEqual(lines[2], b'Content-Length: 43') - self.assertEqual(lines[3], b'Content-Type: text/plain') - self.assertTrue(lines[4]) - self.assertEqual(lines[5], b'Server: waitress') - self.assertEqual(lines[6], b'Too Ugly') - self.assertEqual(lines[7], b'body') - self.assertEqual(lines[8], b'(generated by waitress)') - - def test_execute_http_11(self): - inst = self._makeOne() - inst.version = '1.1' - inst.execute() - lines = filter_lines(inst.channel.written) - self.assertEqual(len(lines), 8) - self.assertEqual(lines[0], b'HTTP/1.1 432 Too Ugly') - self.assertEqual(lines[1], b'Content-Length: 43') - self.assertEqual(lines[2], b'Content-Type: text/plain') - self.assertTrue(lines[3]) - self.assertEqual(lines[4], b'Server: waitress') - self.assertEqual(lines[5], b'Too Ugly') - self.assertEqual(lines[6], b'body') - self.assertEqual(lines[7], b'(generated by waitress)') - - def test_execute_http_11_close(self): - inst = self._makeOne() - inst.version = '1.1' - inst.request.headers['CONNECTION'] = 'close' - inst.execute() - lines = filter_lines(inst.channel.written) - self.assertEqual(len(lines), 9) - self.assertEqual(lines[0], b'HTTP/1.1 432 Too Ugly') - self.assertEqual(lines[1], b'Connection: close') - self.assertEqual(lines[2], b'Content-Length: 43') - self.assertEqual(lines[3], b'Content-Type: text/plain') - self.assertTrue(lines[4]) - self.assertEqual(lines[5], b'Server: waitress') - self.assertEqual(lines[6], b'Too Ugly') - self.assertEqual(lines[7], b'body') - self.assertEqual(lines[8], b'(generated by waitress)') - - def test_execute_http_11_keep(self): - inst = self._makeOne() - inst.version = '1.1' - inst.request.headers['CONNECTION'] = 'keep-alive' - inst.execute() - lines = filter_lines(inst.channel.written) - self.assertEqual(len(lines), 8) - self.assertEqual(lines[0], b'HTTP/1.1 432 Too Ugly') - self.assertEqual(lines[1], b'Content-Length: 43') - self.assertEqual(lines[2], b'Content-Type: text/plain') - self.assertTrue(lines[3]) - self.assertEqual(lines[4], b'Server: waitress') - self.assertEqual(lines[5], b'Too Ugly') - self.assertEqual(lines[6], b'body') - self.assertEqual(lines[7], b'(generated by waitress)') - - -class DummyError(object): - code = '432' - reason = 'Too Ugly' - body = 'body' - -class DummyTask(object): - serviced = False - deferred = False - cancelled = False - - def __init__(self, toraise=None): - self.toraise = toraise - - def service(self): - self.serviced = True - if self.toraise: - raise self.toraise - - def defer(self): - self.deferred = True - if self.toraise: - raise self.toraise - - def cancel(self): - self.cancelled = True - -class DummyAdj(object): - log_socket_errors = True - ident = 'waitress' - host = '127.0.0.1' - port = 80 - url_prefix = '' - trusted_proxy = None - -class DummyServer(object): - server_name = 'localhost' - effective_port = 80 - - def __init__(self): - self.adj = DummyAdj() - -class DummyChannel(object): - closed_when_done = False - adj = DummyAdj() - creation_time = 0 - addr = ['127.0.0.1'] - - def __init__(self, server=None): - if server is None: - server = DummyServer() - self.server = server - self.written = b'' - self.otherdata = [] - - def write_soon(self, data): - if isinstance(data, bytes): - self.written += data - else: - self.otherdata.append(data) - return len(data) - -class DummyParser(object): - version = '1.0' - command = 'GET' - path = '/' - query = '' - url_scheme = 'http' - expect_continue = False - headers_finished = False - - def __init__(self): - self.headers = {} - - def get_body_stream(self): - return 'stream' - -def filter_lines(s): - return list(filter(None, s.split(b'\r\n'))) - -class DummyLogger(object): - - def __init__(self): - self.logged = [] - - def warning(self, msg): - self.logged.append(msg) - - def exception(self, msg): - self.logged.append(msg) diff --git a/libs/waitress/tests/test_trigger.py b/libs/waitress/tests/test_trigger.py deleted file mode 100644 index bfff16e4e..000000000 --- a/libs/waitress/tests/test_trigger.py +++ /dev/null @@ -1,105 +0,0 @@ -import unittest -import os -import sys - -if not sys.platform.startswith("win"): - - class Test_trigger(unittest.TestCase): - - def _makeOne(self, map): - from waitress.trigger import trigger - return trigger(map) - - def test__close(self): - map = {} - inst = self._makeOne(map) - fd = os.open(os.path.abspath(__file__), os.O_RDONLY) - inst._fds = (fd,) - inst.close() - self.assertRaises(OSError, os.read, fd, 1) - - def test__physical_pull(self): - map = {} - inst = self._makeOne(map) - inst._physical_pull() - r = os.read(inst._fds[0], 1) - self.assertEqual(r, b'x') - - def test_readable(self): - map = {} - inst = self._makeOne(map) - self.assertEqual(inst.readable(), True) - - def test_writable(self): - map = {} - inst = self._makeOne(map) - self.assertEqual(inst.writable(), False) - - def test_handle_connect(self): - map = {} - inst = self._makeOne(map) - self.assertEqual(inst.handle_connect(), None) - - def test_close(self): - map = {} - inst = self._makeOne(map) - self.assertEqual(inst.close(), None) - self.assertEqual(inst._closed, True) - - def test_handle_close(self): - map = {} - inst = self._makeOne(map) - self.assertEqual(inst.handle_close(), None) - self.assertEqual(inst._closed, True) - - def test_pull_trigger_nothunk(self): - map = {} - inst = self._makeOne(map) - self.assertEqual(inst.pull_trigger(), None) - r = os.read(inst._fds[0], 1) - self.assertEqual(r, b'x') - - def test_pull_trigger_thunk(self): - map = {} - inst = self._makeOne(map) - self.assertEqual(inst.pull_trigger(True), None) - self.assertEqual(len(inst.thunks), 1) - r = os.read(inst._fds[0], 1) - self.assertEqual(r, b'x') - - def test_handle_read_socket_error(self): - map = {} - inst = self._makeOne(map) - result = inst.handle_read() - self.assertEqual(result, None) - - def test_handle_read_no_socket_error(self): - map = {} - inst = self._makeOne(map) - inst.pull_trigger() - result = inst.handle_read() - self.assertEqual(result, None) - - def test_handle_read_thunk(self): - map = {} - inst = self._makeOne(map) - inst.pull_trigger() - L = [] - inst.thunks = [lambda: L.append(True)] - result = inst.handle_read() - self.assertEqual(result, None) - self.assertEqual(L, [True]) - self.assertEqual(inst.thunks, []) - - def test_handle_read_thunk_error(self): - map = {} - inst = self._makeOne(map) - def errorthunk(): - raise ValueError - inst.pull_trigger(errorthunk) - L = [] - inst.log_info = lambda *arg: L.append(arg) - result = inst.handle_read() - self.assertEqual(result, None) - self.assertEqual(len(L), 1) - self.assertEqual(inst.thunks, []) diff --git a/libs/waitress/tests/test_utilities.py b/libs/waitress/tests/test_utilities.py deleted file mode 100644 index 73f6c7b76..000000000 --- a/libs/waitress/tests/test_utilities.py +++ /dev/null @@ -1,121 +0,0 @@ -############################################################################## -# -# Copyright (c) 2002 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## - -import unittest - -class Test_parse_http_date(unittest.TestCase): - - def _callFUT(self, v): - from waitress.utilities import parse_http_date - return parse_http_date(v) - - def test_rfc850(self): - val = 'Tuesday, 08-Feb-94 14:15:29 GMT' - result = self._callFUT(val) - self.assertEqual(result, 760716929) - - def test_rfc822(self): - val = 'Sun, 08 Feb 1994 14:15:29 GMT' - result = self._callFUT(val) - self.assertEqual(result, 760716929) - - def test_neither(self): - val = '' - result = self._callFUT(val) - self.assertEqual(result, 0) - -class Test_build_http_date(unittest.TestCase): - - def test_rountdrip(self): - from waitress.utilities import build_http_date, parse_http_date - from time import time - t = int(time()) - self.assertEqual(t, parse_http_date(build_http_date(t))) - -class Test_unpack_rfc850(unittest.TestCase): - - def _callFUT(self, val): - from waitress.utilities import unpack_rfc850, rfc850_reg - return unpack_rfc850(rfc850_reg.match(val.lower())) - - def test_it(self): - val = 'Tuesday, 08-Feb-94 14:15:29 GMT' - result = self._callFUT(val) - self.assertEqual(result, (1994, 2, 8, 14, 15, 29, 0, 0, 0)) - -class Test_unpack_rfc_822(unittest.TestCase): - - def _callFUT(self, val): - from waitress.utilities import unpack_rfc822, rfc822_reg - return unpack_rfc822(rfc822_reg.match(val.lower())) - - def test_it(self): - val = 'Sun, 08 Feb 1994 14:15:29 GMT' - result = self._callFUT(val) - self.assertEqual(result, (1994, 2, 8, 14, 15, 29, 0, 0, 0)) - -class Test_find_double_newline(unittest.TestCase): - - def _callFUT(self, val): - from waitress.utilities import find_double_newline - return find_double_newline(val) - - def test_empty(self): - self.assertEqual(self._callFUT(b''), -1) - - def test_one_linefeed(self): - self.assertEqual(self._callFUT(b'\n'), -1) - - def test_double_linefeed(self): - self.assertEqual(self._callFUT(b'\n\n'), 2) - - def test_one_crlf(self): - self.assertEqual(self._callFUT(b'\r\n'), -1) - - def test_double_crfl(self): - self.assertEqual(self._callFUT(b'\r\n\r\n'), 4) - - def test_mixed(self): - self.assertEqual(self._callFUT(b'\n\n00\r\n\r\n'), 2) - -class Test_logging_dispatcher(unittest.TestCase): - - def _makeOne(self): - from waitress.utilities import logging_dispatcher - return logging_dispatcher(map={}) - - def test_log_info(self): - import logging - inst = self._makeOne() - logger = DummyLogger() - inst.logger = logger - inst.log_info('message', 'warning') - self.assertEqual(logger.severity, logging.WARN) - self.assertEqual(logger.message, 'message') - -class TestBadRequest(unittest.TestCase): - - def _makeOne(self): - from waitress.utilities import BadRequest - return BadRequest(1) - - def test_it(self): - inst = self._makeOne() - self.assertEqual(inst.body, 1) - -class DummyLogger(object): - - def log(self, severity, message): - self.severity = severity - self.message = message diff --git a/libs/waitress/trigger.py b/libs/waitress/trigger.py deleted file mode 100644 index cac8e2649..000000000 --- a/libs/waitress/trigger.py +++ /dev/null @@ -1,198 +0,0 @@ -############################################################################## -# -# Copyright (c) 2001-2005 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE -# -############################################################################## - -import asyncore -import os -import socket -import errno -import threading - -# Wake up a call to select() running in the main thread. -# -# This is useful in a context where you are using Medusa's I/O -# subsystem to deliver data, but the data is generated by another -# thread. Normally, if Medusa is in the middle of a call to -# select(), new output data generated by another thread will have -# to sit until the call to select() either times out or returns. -# If the trigger is 'pulled' by another thread, it should immediately -# generate a READ event on the trigger object, which will force the -# select() invocation to return. -# -# A common use for this facility: letting Medusa manage I/O for a -# large number of connections; but routing each request through a -# thread chosen from a fixed-size thread pool. When a thread is -# acquired, a transaction is performed, but output data is -# accumulated into buffers that will be emptied more efficiently -# by Medusa. [picture a server that can process database queries -# rapidly, but doesn't want to tie up threads waiting to send data -# to low-bandwidth connections] -# -# The other major feature provided by this class is the ability to -# move work back into the main thread: if you call pull_trigger() -# with a thunk argument, when select() wakes up and receives the -# event it will call your thunk from within that thread. The main -# purpose of this is to remove the need to wrap thread locks around -# Medusa's data structures, which normally do not need them. [To see -# why this is true, imagine this scenario: A thread tries to push some -# new data onto a channel's outgoing data queue at the same time that -# the main thread is trying to remove some] - -class _triggerbase(object): - """OS-independent base class for OS-dependent trigger class.""" - - kind = None # subclass must set to "pipe" or "loopback"; used by repr - - def __init__(self): - self._closed = False - - # `lock` protects the `thunks` list from being traversed and - # appended to simultaneously. - self.lock = threading.Lock() - - # List of no-argument callbacks to invoke when the trigger is - # pulled. These run in the thread running the asyncore mainloop, - # regardless of which thread pulls the trigger. - self.thunks = [] - - def readable(self): - return True - - def writable(self): - return False - - def handle_connect(self): - pass - - def handle_close(self): - self.close() - - # Override the asyncore close() method, because it doesn't know about - # (so can't close) all the gimmicks we have open. Subclass must - # supply a _close() method to do platform-specific closing work. _close() - # will be called iff we're not already closed. - def close(self): - if not self._closed: - self._closed = True - self.del_channel() - self._close() # subclass does OS-specific stuff - - def pull_trigger(self, thunk=None): - if thunk: - with self.lock: - self.thunks.append(thunk) - self._physical_pull() - - def handle_read(self): - try: - self.recv(8192) - except (OSError, socket.error): - return - with self.lock: - for thunk in self.thunks: - try: - thunk() - except: - nil, t, v, tbinfo = asyncore.compact_traceback() - self.log_info( - 'exception in trigger thunk: (%s:%s %s)' % - (t, v, tbinfo)) - self.thunks = [] - -if os.name == 'posix': - - class trigger(_triggerbase, asyncore.file_dispatcher): - kind = "pipe" - - def __init__(self, map): - _triggerbase.__init__(self) - r, self.trigger = self._fds = os.pipe() - asyncore.file_dispatcher.__init__(self, r, map=map) - - def _close(self): - for fd in self._fds: - os.close(fd) - self._fds = [] - - def _physical_pull(self): - os.write(self.trigger, b'x') - -else: # pragma: no cover - # Windows version; uses just sockets, because a pipe isn't select'able - # on Windows. - - class trigger(_triggerbase, asyncore.dispatcher): - kind = "loopback" - - def __init__(self, map): - _triggerbase.__init__(self) - - # Get a pair of connected sockets. The trigger is the 'w' - # end of the pair, which is connected to 'r'. 'r' is put - # in the asyncore socket map. "pulling the trigger" then - # means writing something on w, which will wake up r. - - w = socket.socket() - # Disable buffering -- pulling the trigger sends 1 byte, - # and we want that sent immediately, to wake up asyncore's - # select() ASAP. - w.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) - - count = 0 - while True: - count += 1 - # Bind to a local port; for efficiency, let the OS pick - # a free port for us. - # Unfortunately, stress tests showed that we may not - # be able to connect to that port ("Address already in - # use") despite that the OS picked it. This appears - # to be a race bug in the Windows socket implementation. - # So we loop until a connect() succeeds (almost always - # on the first try). See the long thread at - # http://mail.zope.org/pipermail/zope/2005-July/160433.html - # for hideous details. - a = socket.socket() - a.bind(("127.0.0.1", 0)) - connect_address = a.getsockname() # assigned (host, port) pair - a.listen(1) - try: - w.connect(connect_address) - break # success - except socket.error as detail: - if detail[0] != errno.WSAEADDRINUSE: - # "Address already in use" is the only error - # I've seen on two WinXP Pro SP2 boxes, under - # Pythons 2.3.5 and 2.4.1. - raise - # (10048, 'Address already in use') - # assert count <= 2 # never triggered in Tim's tests - if count >= 10: # I've never seen it go above 2 - a.close() - w.close() - raise RuntimeError("Cannot bind trigger!") - # Close `a` and try again. Note: I originally put a short - # sleep() here, but it didn't appear to help or hurt. - a.close() - - r, addr = a.accept() # r becomes asyncore's (self.)socket - a.close() - self.trigger = w - asyncore.dispatcher.__init__(self, r, map=map) - - def _close(self): - # self.socket is r, and self.trigger is w, from __init__ - self.socket.close() - self.trigger.close() - - def _physical_pull(self): - self.trigger.send(b'x') diff --git a/libs/waitress/utilities.py b/libs/waitress/utilities.py deleted file mode 100644 index 943c92fd1..000000000 --- a/libs/waitress/utilities.py +++ /dev/null @@ -1,216 +0,0 @@ -############################################################################## -# -# Copyright (c) 2004 Zope Foundation and Contributors. -# All Rights Reserved. -# -# This software is subject to the provisions of the Zope Public License, -# Version 2.1 (ZPL). A copy of the ZPL should accompany this distribution. -# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED -# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS -# FOR A PARTICULAR PURPOSE. -# -############################################################################## -"""Utility functions -""" - -import asyncore -import errno -import logging -import os -import re -import stat -import time -import calendar - -logger = logging.getLogger('waitress') - -def find_double_newline(s): - """Returns the position just after a double newline in the given string.""" - pos1 = s.find(b'\n\r\n') # One kind of double newline - if pos1 >= 0: - pos1 += 3 - pos2 = s.find(b'\n\n') # Another kind of double newline - if pos2 >= 0: - pos2 += 2 - - if pos1 >= 0: - if pos2 >= 0: - return min(pos1, pos2) - else: - return pos1 - else: - return pos2 - -def concat(*args): - return ''.join(args) - -def join(seq, field=' '): - return field.join(seq) - -def group(s): - return '(' + s + ')' - -short_days = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'] -long_days = ['sunday', 'monday', 'tuesday', 'wednesday', - 'thursday', 'friday', 'saturday'] - -short_day_reg = group(join(short_days, '|')) -long_day_reg = group(join(long_days, '|')) - -daymap = {} -for i in range(7): - daymap[short_days[i]] = i - daymap[long_days[i]] = i - -hms_reg = join(3 * [group('[0-9][0-9]')], ':') - -months = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', - 'aug', 'sep', 'oct', 'nov', 'dec'] - -monmap = {} -for i in range(12): - monmap[months[i]] = i + 1 - -months_reg = group(join(months, '|')) - -# From draft-ietf-http-v11-spec-07.txt/3.3.1 -# Sun, 06 Nov 1994 08:49:37 GMT ; RFC 822, updated by RFC 1123 -# Sunday, 06-Nov-94 08:49:37 GMT ; RFC 850, obsoleted by RFC 1036 -# Sun Nov 6 08:49:37 1994 ; ANSI C's asctime() format - -# rfc822 format -rfc822_date = join( - [concat(short_day_reg, ','), # day - group('[0-9][0-9]?'), # date - months_reg, # month - group('[0-9]+'), # year - hms_reg, # hour minute second - 'gmt' - ], - ' ' -) - -rfc822_reg = re.compile(rfc822_date) - -def unpack_rfc822(m): - g = m.group - return ( - int(g(4)), # year - monmap[g(3)], # month - int(g(2)), # day - int(g(5)), # hour - int(g(6)), # minute - int(g(7)), # second - 0, - 0, - 0, - ) - -# rfc850 format -rfc850_date = join( - [concat(long_day_reg, ','), - join( - [group('[0-9][0-9]?'), - months_reg, - group('[0-9]+') - ], - '-' - ), - hms_reg, - 'gmt' - ], - ' ' -) - -rfc850_reg = re.compile(rfc850_date) -# they actually unpack the same way -def unpack_rfc850(m): - g = m.group - yr = g(4) - if len(yr) == 2: - yr = '19' + yr - return ( - int(yr), # year - monmap[g(3)], # month - int(g(2)), # day - int(g(5)), # hour - int(g(6)), # minute - int(g(7)), # second - 0, - 0, - 0 - ) - -# parsdate.parsedate - ~700/sec. -# parse_http_date - ~1333/sec. - -weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] -monthname = [None, 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] - -def build_http_date(when): - year, month, day, hh, mm, ss, wd, y, z = time.gmtime(when) - return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( - weekdayname[wd], - day, monthname[month], year, - hh, mm, ss) - -def parse_http_date(d): - d = d.lower() - m = rfc850_reg.match(d) - if m and m.end() == len(d): - retval = int(calendar.timegm(unpack_rfc850(m))) - else: - m = rfc822_reg.match(d) - if m and m.end() == len(d): - retval = int(calendar.timegm(unpack_rfc822(m))) - else: - return 0 - return retval - -class logging_dispatcher(asyncore.dispatcher): - logger = logger - - def log_info(self, message, type='info'): - severity = { - 'info': logging.INFO, - 'warning': logging.WARN, - 'error': logging.ERROR, - } - self.logger.log(severity.get(type, logging.INFO), message) - -def cleanup_unix_socket(path): - try: - st = os.stat(path) - except OSError as exc: - if exc.errno != errno.ENOENT: - raise # pragma: no cover - else: - if stat.S_ISSOCK(st.st_mode): - try: - os.remove(path) - except OSError: # pragma: no cover - # avoid race condition error during tests - pass - -class Error(object): - - def __init__(self, body): - self.body = body - -class BadRequest(Error): - code = 400 - reason = 'Bad Request' - -class RequestHeaderFieldsTooLarge(BadRequest): - code = 431 - reason = 'Request Header Fields Too Large' - -class RequestEntityTooLarge(BadRequest): - code = 413 - reason = 'Request Entity Too Large' - -class InternalServerError(Error): - code = 500 - reason = 'Internal Server Error' diff --git a/screenshot/0-wizard/wizard-1- general.png b/screenshot/0-wizard/wizard-1- general.png new file mode 100644 index 000000000..e1da53a53 Binary files /dev/null and b/screenshot/0-wizard/wizard-1- general.png differ diff --git a/screenshot/0-wizard/wizard-2-subliminal.png b/screenshot/0-wizard/wizard-2-subliminal.png new file mode 100644 index 000000000..47af9f776 Binary files /dev/null and b/screenshot/0-wizard/wizard-2-subliminal.png differ diff --git a/screenshot/0-wizard/wizard-3-sonarr.png b/screenshot/0-wizard/wizard-3-sonarr.png new file mode 100644 index 000000000..9325f0efc Binary files /dev/null and b/screenshot/0-wizard/wizard-3-sonarr.png differ diff --git a/screenshot/0-wizard/wizard-4-radarr.png b/screenshot/0-wizard/wizard-4-radarr.png new file mode 100644 index 000000000..f77e3b2b9 Binary files /dev/null and b/screenshot/0-wizard/wizard-4-radarr.png differ diff --git a/screenshot/1-series/series-1-list.png b/screenshot/1-series/series-1-list.png new file mode 100644 index 000000000..08a399cae Binary files /dev/null and b/screenshot/1-series/series-1-list.png differ diff --git a/screenshot/1-series/series-2-episodes.png b/screenshot/1-series/series-2-episodes.png new file mode 100644 index 000000000..860ff6285 Binary files /dev/null and b/screenshot/1-series/series-2-episodes.png differ diff --git a/screenshot/1-series/series-3-manual.png b/screenshot/1-series/series-3-manual.png new file mode 100644 index 000000000..ed8f3b4b0 Binary files /dev/null and b/screenshot/1-series/series-3-manual.png differ diff --git a/screenshot/1-series/series-4-config.png b/screenshot/1-series/series-4-config.png new file mode 100644 index 000000000..03ae100b0 Binary files /dev/null and b/screenshot/1-series/series-4-config.png differ diff --git a/screenshot/2-movies/movies-1-list.png b/screenshot/2-movies/movies-1-list.png new file mode 100644 index 000000000..0456477ba Binary files /dev/null and b/screenshot/2-movies/movies-1-list.png differ diff --git a/screenshot/2-movies/movies-2-movie.png b/screenshot/2-movies/movies-2-movie.png new file mode 100644 index 000000000..078b29c30 Binary files /dev/null and b/screenshot/2-movies/movies-2-movie.png differ diff --git a/screenshot/2-movies/movies-3-manual.png b/screenshot/2-movies/movies-3-manual.png new file mode 100644 index 000000000..826b7cd56 Binary files /dev/null and b/screenshot/2-movies/movies-3-manual.png differ diff --git a/screenshot/2-movies/movies-4-config.png b/screenshot/2-movies/movies-4-config.png new file mode 100644 index 000000000..8f9b7c88e Binary files /dev/null and b/screenshot/2-movies/movies-4-config.png differ diff --git a/screenshot/3-history/history-1-series.png b/screenshot/3-history/history-1-series.png new file mode 100644 index 000000000..145afd413 Binary files /dev/null and b/screenshot/3-history/history-1-series.png differ diff --git a/screenshot/3-history/history-2-movies.png b/screenshot/3-history/history-2-movies.png new file mode 100644 index 000000000..085cb6705 Binary files /dev/null and b/screenshot/3-history/history-2-movies.png differ diff --git a/screenshot/4-wanted/wanted-1-series.png b/screenshot/4-wanted/wanted-1-series.png new file mode 100644 index 000000000..b64d86719 Binary files /dev/null and b/screenshot/4-wanted/wanted-1-series.png differ diff --git a/screenshot/4-wanted/wanted-2-movies.png b/screenshot/4-wanted/wanted-2-movies.png new file mode 100644 index 000000000..8c09403b5 Binary files /dev/null and b/screenshot/4-wanted/wanted-2-movies.png differ diff --git a/screenshot/5-settings/settings-1-general.png b/screenshot/5-settings/settings-1-general.png new file mode 100644 index 000000000..362efc4ea Binary files /dev/null and b/screenshot/5-settings/settings-1-general.png differ diff --git a/screenshot/5-settings/settings-2-sonarr.png b/screenshot/5-settings/settings-2-sonarr.png new file mode 100644 index 000000000..0f3c2fbd2 Binary files /dev/null and b/screenshot/5-settings/settings-2-sonarr.png differ diff --git a/screenshot/5-settings/settings-3-radarr.png b/screenshot/5-settings/settings-3-radarr.png new file mode 100644 index 000000000..d358885c7 Binary files /dev/null and b/screenshot/5-settings/settings-3-radarr.png differ diff --git a/screenshot/5-settings/settings-4-subliminal.png b/screenshot/5-settings/settings-4-subliminal.png new file mode 100644 index 000000000..07d077b03 Binary files /dev/null and b/screenshot/5-settings/settings-4-subliminal.png differ diff --git a/screenshot/5-settings/settings-5-notifications.png b/screenshot/5-settings/settings-5-notifications.png new file mode 100644 index 000000000..e443b110b Binary files /dev/null and b/screenshot/5-settings/settings-5-notifications.png differ diff --git a/screenshot/6-system/system-1-tasks.png b/screenshot/6-system/system-1-tasks.png new file mode 100644 index 000000000..fe831a1f2 Binary files /dev/null and b/screenshot/6-system/system-1-tasks.png differ diff --git a/screenshot/6-system/system-2-logs.png b/screenshot/6-system/system-2-logs.png new file mode 100644 index 000000000..2088f0e98 Binary files /dev/null and b/screenshot/6-system/system-2-logs.png differ diff --git a/screenshot/6-system/system-3-releases.png b/screenshot/6-system/system-3-releases.png new file mode 100644 index 000000000..f681fc1ac Binary files /dev/null and b/screenshot/6-system/system-3-releases.png differ diff --git a/screenshot/episodes.png b/screenshot/episodes.png deleted file mode 100644 index c5ec6e12d..000000000 Binary files a/screenshot/episodes.png and /dev/null differ diff --git a/screenshot/history.png b/screenshot/history.png deleted file mode 100644 index 65caa157d..000000000 Binary files a/screenshot/history.png and /dev/null differ diff --git a/screenshot/series.png b/screenshot/series.png deleted file mode 100644 index 70e1b28e1..000000000 Binary files a/screenshot/series.png and /dev/null differ diff --git a/screenshot/settings.png b/screenshot/settings.png deleted file mode 100644 index 84508c156..000000000 Binary files a/screenshot/settings.png and /dev/null differ diff --git a/screenshot/system.png b/screenshot/system.png deleted file mode 100644 index cb795ab1c..000000000 Binary files a/screenshot/system.png and /dev/null differ diff --git a/screenshot/wanted.png b/screenshot/wanted.png deleted file mode 100644 index 4da27a4c3..000000000 Binary files a/screenshot/wanted.png and /dev/null differ diff --git a/views/episodes.tpl b/views/episodes.tpl index 7314e3946..8e11f50fe 100644 --- a/views/episodes.tpl +++ b/views/episodes.tpl @@ -1,552 +1,577 @@ - - - - - - - - - - - - - - - - - - - - - - - {{details[0]}} - Bazarr - - - - - - %import ast - %from get_languages import * - %from get_settings import * - %single_language = get_general_settings()[7] -
-
-
Loading...
-
- % include('menu.tpl') - -
-
- -
- - - <% - subs_languages = ast.literal_eval(str(details[7])) - subs_languages_list = [] - if subs_languages is not None: - for subs_language in subs_languages: - subs_languages_list.append(subs_language) - end - end - %> - -
-

{{details[0]}}

-

{{details[1]}}

-

-

{{details[6]}}
-
{{details[8]}}
-
{{number}} files
-

-

- %for language in subs_languages_list: -

{{language}}
- %end -

-
-
- - %if len(seasons) == 0: -
-

No episode files available for this series or Bazarr is still synchronizing with Sonarr. Please come back later.

-
- %else: - %for season in seasons: -
- %missing_subs = len([i for i in season if i[6] != "[]"]) - %total_subs = len(season) - %subs_label = '' - %if subs_languages is not None: - % subs_label = '
' + str(total_subs - missing_subs) + ' / ' + str(total_subs) + '
' - %end -

Season {{season[0][2]}}{{!subs_label}}

-
-
-
-
-

- Show/Hide Episodes

-
-
-
-
- - - - - - - - - - - - - %for episode in season: - - - - - - - - - %end - -
EpisodeTitleExisting
subtitles
Missing
subtitles
Manual
search
- %if episode[9] == 'True': - - %else: - - %end - {{episode[3]}}{{episode[0]}} - %if episode[4] is not None: - % actual_languages = ast.literal_eval(episode[4]) - % actual_languages.sort() - %else: - % actual_languages = '[]' - %end - %try: - %for language in actual_languages: - %if language[1] is not None: - - {{language[0]}} - - - %else: -
- {{language[0]}} -
- %end - %end - %except: - %pass - %end -
- %try: - %if episode[6] is not None: - % missing_languages = ast.literal_eval(episode[6]) - % missing_languages.sort() - %else: - % missing_languages = None - %end - %if missing_languages is not None: - %for language in missing_languages: - - {{language}} - - - %end - %end - %except: - %pass - %end - - %if subs_languages is not None: - - %end -
-
-
-
- %end - %end -
- - - - - - % include('footer.tpl') - - - - + + + + + + + + + + + + + + + + + + + + + + + {{details[0]}} - Bazarr + + + + + + %import ast + %from get_languages import * + %from get_settings import * + %single_language = get_general_settings()[7] +
+
+
Loading...
+
+ % include('menu.tpl') + +
+
+ +
+ + + <% + subs_languages = ast.literal_eval(str(details[7])) + subs_languages_list = [] + if subs_languages is not None: + for subs_language in subs_languages: + subs_languages_list.append(subs_language) + end + end + %> + +
+

{{details[0]}}

+

{{details[1]}}

+

+

{{details[6]}}
+
{{details[8]}}
+
{{number}} files
+

+

+ %for language in subs_languages_list: +

{{language}}
+ %end +

+
+
+ + %if len(seasons) == 0: +
+

No episode files available for this series or Bazarr is still synchronizing with Sonarr. Please come back later.

+
+ %else: + %for season in seasons: +
+ %missing_subs = len([i for i in season if i[6] != "[]"]) + %total_subs = len(season) + %subs_label = '' + %if subs_languages is not None: + % subs_label = '
' + str(total_subs - missing_subs) + ' / ' + str(total_subs) + '
' + %end +

Season {{season[0][2]}}{{!subs_label}}

+
+
+
+
+

+ Show/Hide Episodes

+
+
+
+
+ + + + + + + + + + + + + %for episode in season: + + + + + + + + + %end + +
EpisodeTitleExisting
subtitles
Missing
subtitles
Manual
search
+ %if episode[9] == 'True': + + %else: + + %end + {{episode[3]}} + % if episode[8] is not None: + + % end + {{episode[0]}} + + %if episode[4] is not None: + % actual_languages = ast.literal_eval(episode[4]) + % actual_languages.sort() + %else: + % actual_languages = '[]' + %end + %try: + %for language in actual_languages: + %if language[1] is not None: + + {{language[0]}} + + + %else: +
+ {{language[0]}} +
+ %end + %end + %except: + %pass + %end +
+ %try: + <% + if episode[6] is not None: + missing_languages = ast.literal_eval(episode[6]) + missing_languages.sort() + end + if missing_languages is not None: + from get_subtitle import search_active + from get_settings import get_general_settings + for language in missing_languages: + if episode[10] is not None and get_general_settings()[25] and language in episode[10]: + for lang in ast.literal_eval(episode[10]): + if language in lang: + if search_active(lang[1]): + %> + + {{language}} + + + %else: + + {{language}} + + + %end + %end + %end + %else: + + {{language}} + + + %end + %end + %end + %except: + %pass + %end + + %if subs_languages is not None: + + %end +
+
+
+
+ %end + %end +
+ + + + + + % include('footer.tpl') + + + + diff --git a/views/historymovies.tpl b/views/historymovies.tpl index e25b537e7..a8b6c6f22 100644 --- a/views/historymovies.tpl +++ b/views/historymovies.tpl @@ -67,11 +67,11 @@ %if row[0] == 0: -
+
%elif row[0] == 1: -
+
%end @@ -80,7 +80,7 @@ {{row[1]}} -
+
{{pretty.date(int(row[2]))}}
diff --git a/views/historyseries.tpl b/views/historyseries.tpl index 0542f9ba2..e1c85ba19 100644 --- a/views/historyseries.tpl +++ b/views/historyseries.tpl @@ -69,11 +69,11 @@ %if row[0] == 0: -
+
%elif row[0] == 1: -
+
%end @@ -95,7 +95,7 @@ %end -
+
{{pretty.date(int(row[4]))}}
diff --git a/views/logs.tpl b/views/logs.tpl index ea8fe473f..b8ec92f0a 100644 --- a/views/logs.tpl +++ b/views/logs.tpl @@ -34,25 +34,27 @@ %line = log.split('|') \\ %try: -{{line[2]}}\\ +{{line[3]}}\\ %except: \\ %end diff --git a/views/movie.tpl b/views/movie.tpl index a4afd0489..de6e07957 100644 --- a/views/movie.tpl +++ b/views/movie.tpl @@ -120,6 +120,9 @@

{{details[6]}}
{{details[8]}}
+ % if details[12] is not None: +
{{details[12]}}
+ % end

%for language in subs_languages_list: @@ -167,7 +170,12 @@ <% - missing_subs_languages = ast.literal_eval(str(details[11])) + if details[11] is not None: + missing_subs_languages = ast.literal_eval(details[11]) + else: + missing_subs_languages = [] + end + from get_subtitle import search_active if missing_subs_languages is not None: %> @@ -179,12 +187,32 @@
<% for missing_subs_language in missing_subs_languages: + if details[14] is not None and get_general_settings()[25] and missing_subs_language in details[14]: + for lang in ast.literal_eval(details[14]): + if missing_subs_language in lang: + if search_active(lang[1]): %> {{language_from_alpha2(str(missing_subs_language))}} + %else: + + {{language_from_alpha2(str(missing_subs_language))}} + + <% + end + end + end + else: + %> + + {{language_from_alpha2(str(missing_subs_language))}} + + + <% + end end end %> diff --git a/views/movies.tpl b/views/movies.tpl index 3a321fbb7..fb4391841 100644 --- a/views/movies.tpl +++ b/views/movies.tpl @@ -72,17 +72,22 @@ %if row[8] == "True": - + %else: - + %end - {{row[1]}} + + % if row[9] is not None: + + % end + {{row[1]}} + %if os.path.isfile(row[2]): - + %else: - + %end {{row[2]}} @@ -105,7 +110,7 @@ end end %> -

+
diff --git a/views/series.tpl b/views/series.tpl index ca680ceae..62330c5db 100644 --- a/views/series.tpl +++ b/views/series.tpl @@ -79,9 +79,9 @@ {{row[1]}} %if os.path.isdir(row[2]): - + %else: - + %end {{row[2]}} @@ -127,7 +127,7 @@ end end %> -
+
diff --git a/views/settings.tpl b/views/settings.tpl index b698ce56b..bdbbff1fb 100644 --- a/views/settings.tpl +++ b/views/settings.tpl @@ -33,6 +33,9 @@ opacity: 0.45 !important; pointer-events: none !important; } + [data-tooltip]:after { + z-index: 2; + } @@ -135,22 +138,12 @@
- +
- -
- -